<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Chambers</title>
    <description>The latest articles on DEV Community by Michael Chambers (@mchambers).</description>
    <link>https://dev.to/mchambers</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F33054%2F8df0f618-7437-4130-8821-091fe4daaead.jpg</url>
      <title>DEV Community: Michael Chambers</title>
      <link>https://dev.to/mchambers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mchambers"/>
    <language>en</language>
    <item>
      <title>Building PromoForge: my solution to promoting a small YouTube channel</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Wed, 14 Oct 2026 21:57:51 +0000</pubDate>
      <link>https://dev.to/mchambers/building-promoforge-my-solution-to-promoting-a-small-youtube-channel-fic</link>
      <guid>https://dev.to/mchambers/building-promoforge-my-solution-to-promoting-a-small-youtube-channel-fic</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the build log from PromoForge’s first ten days. For what it does now, see the &lt;a href="https://dev.to/projects/promoforge/"&gt;PromoForge project page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over the past few years I’ve been running a gaming YouTube channel, &lt;a href="https://www.youtube.com/@ChambersPlaysGames" rel="noopener noreferrer"&gt;Chambers Plays Games&lt;/a&gt;, and like most small creators I’ve found that making the content is only half the battle. The other half is the relentless work of promoting it… writing tweets, scheduling posts, engaging with other channels, and keeping track of which accounts in your following list are actually worth keeping around. I was doing all of this manually, which was both time-consuming and inconsistent. So, as I’ve done more than once before with problems that annoy me enough, I decided to write a tool to solve it.&lt;/p&gt;

&lt;p&gt;That tool is PromoForge.&lt;/p&gt;

&lt;p&gt;This isn’t my first attempt at Twitter automation. Back in early 2018, as part of a &lt;a href="https://michaelmchambers.co.uk/30daydev-january/" rel="noopener noreferrer"&gt;#30DayDev&lt;/a&gt; challenge, I built and open-sourced the &lt;a href="https://michaelmchambers.co.uk/microcosm-twitter-bot/" rel="noopener noreferrer"&gt;Microcosm retweet bot&lt;/a&gt;, a Python and Tweepy script that retweeted content from a configurable set of hashtags on a schedule. It did one thing and did it reasonably well, and I &lt;a href="https://michaelmchambers.co.uk/microcosm-twitter-bot/" rel="noopener noreferrer"&gt;wrote about it on this blog at the time&lt;/a&gt;. The &lt;a href="https://github.com/MChambers1992/retweet-bot" rel="noopener noreferrer"&gt;repository&lt;/a&gt; is now archived. PromoForge is in some ways the natural successor to that project, built on the same instinct to automate the repetitive parts of being online, just considerably more ambitious about what “automated” actually means.&lt;/p&gt;

&lt;p&gt;What started as a fairly simple idea (fetching my YouTube videos and generating tweets for them) has grown into something considerably more ambitious over the last ten days. This post covers what’s been built, some of the more interesting challenges I ran into along the way, and where I plan to take it next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is PromoForge?
&lt;/h2&gt;

&lt;p&gt;At its core, PromoForge is a promotion and audience-growth platform built specifically for gaming content creators. It has two entry points: a command-line tool for headless operation, and a full web UI that you run locally and access through your browser. The web interface is where most of the action happens and it covers three broad areas of work: content scheduling, audience growth, and channel maintenance.&lt;/p&gt;

&lt;p&gt;The scheduling side of things works by connecting to the YouTube Data API, fetching your public videos, and then generating algorithm-optimised tweets for each one. It detects which game is being played from the video title using a combination of exact matching and fuzzy search, selects appropriate hashtags from a curated set, and composes a tweet from a template library. You can export everything as a CSV or push directly to Buffer or X’s own scheduling API. The whole flow from “fetch my videos” to “tweets are scheduled” can be done in a couple of clicks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwg52lvivqtyf5pxvjel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwg52lvivqtyf5pxvjel.png" alt="The tweet generation interface showing video source options, days to look back, number of tweets, start date, posting time, and tweet spacing settings" width="800" height="500"&gt;&lt;/a&gt;The generation panel. You choose the video source (recent videos, full history, highest viewed, etc.), set your schedule preferences, and hit Fetch &amp;amp; Generate. The AI enhancement feature can then refine each tweet using the Anthropic API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing an Audience on Two Platforms at Once
&lt;/h2&gt;

&lt;p&gt;The feature set I’m most pleased with is the audience growth tooling, because it’s the thing I spent the most time doing manually before building this. PromoForge handles both X and YouTube growth independently but through a consistent plan-then-execute workflow. It discovers accounts or channels that match your niche, presents them for review, and only takes action when you give the go-ahead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvmacu2h5jfc5goghgli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvmacu2h5jfc5goghgli.png" alt="The X Audience tab showing discovery criteria including bio keyword search, follower range filters, minimum quality score, and a Discover and Plan button" width="800" height="500"&gt;&lt;/a&gt;X Audience growth. You provide keywords (or a competitor’s account), set follower range filters, and set a minimum quality score. PromoForge plans the follows and lets you review before anything is executed.&lt;/p&gt;

&lt;p&gt;On the X side, you can discover accounts by bio keyword or by pulling from a competitor’s following list. Every account gets quality-scored on a 0–100 scale using signals like follower/following ratio, last activity date, bio content, and spam indicators. Anything below your configured threshold gets pre-deselected in the plan, so you’re never blindly following bots. There’s a daily cap on follows and replies to keep things within safe limits, and the tool randomises action delays between executions to avoid looking like a script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7gb35rlpfwo5y0k80nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7gb35rlpfwo5y0k80nm.png" alt="The YouTube Audience tab showing search keywords, subscriber range filters, query templates with game placeholders, and niche keywords for channel scoring" width="800" height="500"&gt;&lt;/a&gt;YouTube Audience. Searches for channels in your niche using keyword templates, scores them for quality, and plans subscribe and comment actions for your review.&lt;/p&gt;

&lt;p&gt;The YouTube side works similarly but through the YouTube Data API. PromoForge searches for channels matching your keywords, scores each one using metrics like subscriber count, upload frequency, engagement ratio, and niche relevance, then plans subscribe and comment actions. Comments are generated contextually per video, not just copy-pasted from a generic template, which is something I felt was important to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Owning Your Own Channel Engagement
&lt;/h2&gt;

&lt;p&gt;One feature that I didn’t initially plan but ended up building fairly early on is the Own Channel engagement tab. The idea is simple: liking and commenting on your own channel’s videos provides engagement signals that the YouTube algorithm responds to. PromoForge connects via Google OAuth, fetches videos that haven’t been processed yet, and posts game-aware comments and likes automatically. It keeps a log of every video it’s touched so it never double-processes, and the comment templates are keyed by game name so the comments actually look appropriate rather than generic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz21xtq4r2he7o8exlovk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz21xtq4r2he7o8exlovk.png" alt="The Own Channel tab showing fetch mode options, days to look back, max comments field, and toggles for like videos and comment on videos actions" width="800" height="500"&gt;&lt;/a&gt;Own Channel engagement. Fetches unprocessed videos from your channel and likes or comments on them using contextual game-aware templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing Who You’re Actually Following
&lt;/h2&gt;

&lt;p&gt;The Follower Audit is probably the feature with the most immediate practical value. If you’ve been active on X for any length of time, your following list is almost certainly full of inactive accounts, bots, and people who followed you once during a follow-back campaign and haven’t posted since 2019. The audit fetches every account you follow, runs the same quality scoring algorithm used by the growth tools, and classifies them into five tiers: Unfollow, Review, Monitor, Keep, and High Value.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5qsn73j8hxrjq15dfcq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy5qsn73j8hxrjq15dfcq.png" alt="The Follower Audit tab showing a follower growth timeline chart and a Fetch and Score Following button" width="800" height="500"&gt;&lt;/a&gt;Follower Audit. The growth timeline tracks your follower count over time with markers for follow campaigns and video drops. Below it, the audit tool scores and classifies every account you follow.&lt;/p&gt;

&lt;p&gt;Once the audit is complete you can bulk unfollow entire tiers, which is considerably faster than doing it manually through X’s own interface. There’s a protected accounts list you can configure to make sure certain accounts can never be unfollowed regardless of their score, which is useful for friends and collaborators whose accounts might look suspicious to an automated scoring system but clearly aren’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Activity Feed and Settings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yya5730llr9bwhl9fxq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yya5730llr9bwhl9fxq.png" alt="The Activity Audit tab showing a cross-feature event feed" width="800" height="500"&gt;&lt;/a&gt;The Activity Audit tab aggregates events from every feature: follows, comments, audit results, scheduling actions, all in a single chronological feed.&lt;/p&gt;

&lt;p&gt;Because PromoForge operates across multiple platforms and features simultaneously, it has an activity feed that aggregates events from everything into one place. It’s useful for getting a quick read on what’s happened recently without digging into individual feature logs. The settings panel is extensive. It covers everything from YouTube API configuration and tweet templates through to the scoring weights used by the audit and growth algorithms, all of which are configurable through the UI rather than requiring you to touch code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten Days, Eleven Commits, and ~17,000 Lines of Code
&lt;/h2&gt;

&lt;p&gt;The first commit landed on 21st May 2026. The most recent was on 31st May 2026. That’s ten days of development, eleven commits, and what turned out to be a considerably larger project than I initially anticipated. Here’s what the codebase looks like at this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;37 Python source files&lt;/strong&gt; across app logic, YouTube, Twitter, publishing, and route layers, totalling &lt;strong&gt;6,450 lines&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5,779 lines of JavaScript&lt;/strong&gt; in a single app.js (the entire frontend is a single-page app)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,699 lines of Jinja2 HTML&lt;/strong&gt; template for the web UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;781 lines of CSS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19 test files&lt;/strong&gt; with &lt;strong&gt;199 test functions&lt;/strong&gt; covering &lt;strong&gt;2,503 lines&lt;/strong&gt; of unit and integration tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;71 API endpoints&lt;/strong&gt; across 10 Flask route blueprints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~17,212 lines total&lt;/strong&gt; across the entire codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commit history tells the story fairly clearly. The first couple of days were the initial foundation: YouTube API integration, tweet generation, and CSV export. By day three the cross-platform growth tooling was starting to take shape. Then there was a gap before a very productive sprint on the 29th of May where five commits landed covering UI improvements, automated YouTube and Twitter growth, and the project roadmap. The final two commits added rate limiting, a code review improvement plan, comprehensive test coverage, and API resiliency improvements.&lt;/p&gt;

&lt;p&gt;The pace was fast enough that the architecture had to be reasonably disciplined from the start to avoid becoming a mess. I settled on a Flask blueprint structure where each feature area lives in its own module, which made it easy to add new capabilities without the different parts of the codebase getting tangled together. The YouTube and Twitter client code, the publishing logic, and the route handlers all live in separate packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges Worth Mentioning
&lt;/h2&gt;

&lt;p&gt;Not everything went smoothly, and a few problems were interesting enough to be worth writing up.&lt;/p&gt;

&lt;p&gt;The trickiest single thing was the Twitter/X integration. X’s official API has become increasingly restrictive and expensive, so PromoForge uses twikit, a library that operates through the same browser-session GraphQL API that the X web client uses. This works well in practice but twikit’s internals are fragile in a couple of places, and I ended up patching its &lt;code&gt;User.__init__&lt;/code&gt; method and transaction-ID generation at import time to handle cases where the API returns unexpected response shapes. It’s not the kind of thing you’d usually want to do but it was the right call here, since the alternative was either maintaining a fork of twikit or going without the features entirely.&lt;/p&gt;

&lt;p&gt;The hashtag selection logic was another thing that required more thought than expected. The X algorithm actively penalises tweets with too many hashtags, so there’s a hard assertion in the code that caps tweet hashtags at two. The selection is randomised per tweet across four modes: game tag plus a niche tag, game tag only, niche tag only, or a fallback to a generic gaming tag when no game is detected. Getting this balance right, enough context without looking spammy, took a few iterations.&lt;/p&gt;

&lt;p&gt;YouTube quota management was a persistent headache throughout development. The YouTube Data API v3 has a daily unit quota (10,000 units, resetting at PST midnight) and different operations cost different amounts. Fetching videos, searching for channels, and fetching channel details all have different costs, and a careless combination of features could burn through the daily quota faster than expected. PromoForge now tracks quota usage broken down by feature with a live display in the UI, and the growth and engagement flows are designed to be conservative about how many API calls they make in a single run.&lt;/p&gt;

&lt;p&gt;The Buffer API was also less straightforward than their documentation suggests. Buffer uses custom GraphQL scalar types (&lt;code&gt;ChannelId!&lt;/code&gt;, &lt;code&gt;OrganizationId!&lt;/code&gt;, &lt;code&gt;DateTime!&lt;/code&gt;) that have to be inlined directly in the query rather than passed as variables, which isn’t immediately obvious and took some experimentation to get right. The API token situation is also worth being aware of: it’s a personal key from their settings page with a 30-day expiry, not an OAuth token, and using the old OAuth format just silently fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Coming Next
&lt;/h2&gt;

&lt;p&gt;The project roadmap currently breaks down into a few areas. On the content side, I want to add support for threads (multi-tweet sequences rather than single posts) and explore AI-assisted caption generation for YouTube descriptions and community posts, not just tweets. The AI enhancement feature already exists for individual tweets using the Anthropic API, so the foundation is there.&lt;/p&gt;

&lt;p&gt;For audience growth, the plan is to add analytics tracking so you can see which growth actions actually convert into followers and subscribers over time, rather than just tracking raw counts. I’d also like to add competitor analysis on the YouTube side, specifically being able to see who’s subscribing to similar channels and target those viewers directly.&lt;/p&gt;

&lt;p&gt;Longer term, I’ve been thinking about whether PromoForge makes sense as a multi-channel tool, supporting Bluesky and TikTok alongside X and YouTube. The architecture is already reasonably well-separated between platforms, so it wouldn’t require a fundamental redesign, just additional client implementations.&lt;/p&gt;

&lt;p&gt;There’s also the question of whether this could be useful to other creators beyond just my own channel. Right now it’s a self-hosted tool that requires a local Python environment to run. Making it accessible to people who aren’t comfortable with that would be a significant undertaking, but it’s something I’m keeping in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;What I find most interesting about this project, looking back at ten days of work, is how much the scope expanded once the core was in place. The original idea was genuinely just “make tweet scheduling less tedious.” But once you have a YouTube API integration and a Twitter client sitting there, it becomes obvious that there are a dozen adjacent problems you could solve with the same building blocks. The audience growth tools, the audit feature, the own-channel engagement; none of those were in the original spec, but they all made sense to add once the infrastructure existed to support them.&lt;/p&gt;

&lt;p&gt;Whether all of those features survive long-term in their current form, or get refined or replaced as I actually use the tool in practice, remains to be seen. But that’s the nature of building something for yourself. You find out very quickly what actually matters and what seemed like a good idea at the time. I’ll post an update once PromoForge has had a few weeks of real-world use.&lt;/p&gt;

&lt;p&gt;The project isn’t open source at the moment, whether it ever becomes available or if I try to release it as a SaaS remains to be seen. You can follow progress on &lt;a href="https://x.com/EpicChambers" rel="noopener noreferrer"&gt;@EpicChambers&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>projects</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The Microcosm Framework</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Wed, 14 Oct 2026 21:57:34 +0000</pubDate>
      <link>https://dev.to/mchambers/the-microcosm-framework-15ek</link>
      <guid>https://dev.to/mchambers/the-microcosm-framework-15ek</guid>
      <description>&lt;p&gt;Over the course of my past few game development projects, I’ve had to write a lot of code multiple times and every time I’ve been slightly more inconvenienced by it. This ends up wasting valuable time that could be spent doing fun stuff. So I decided enough was enough and I’d create a reusable framework for use with all of my future projects. The premise was simple, I’d outline a set of basic requirements that every project I work on will have and then create a unity package I can import at the start of development to avoid having to try bodge implementations from other projects into a new one.&lt;/p&gt;

&lt;p&gt;The initial requirements were simple; a menu system, basic &lt;a href="https://developers.google.com/admob/unity/start" rel="noopener noreferrer"&gt;AdMob&lt;/a&gt; integration, generic object pooling and simple game management systems to track state/play sounds/etc. These were picked as the requirements because they were the things that I had found the most tedious to set up fresh in every project. However, as I began implementing these features it dawned on me that this limited scope would definitely need to be expanded, what if I wanted Analytics in my games? I would need to first update the framework and then update the package in all subsequent projects it was used in. With the thought of a tedious upgrade process in mind, I went back to the drawing board for my requirements. The final scope was far bigger but added far more value to my future projects, these components and why I chose them are explained in detail below.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://developers.google.com/analytics/devguides/collection/unity/v4/devguide" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Having Google Analytics in all my projects hit me while I was checking the ad data for my project &lt;a href="https://michaelmchambers.co.uk/projects/super-blasty-blasty/" rel="noopener noreferrer"&gt;Super Blasty Blasty&lt;/a&gt;. It dawned on me that the data provided by AdMob wasn’t nearly detailed enough, I wanted the ability to break down how a player was interacting with the games in great detail. I’ve always been a fan of Google Analytics for websites so it was a natural fit to use it in my games as well. The data I can obtain from each project will be a potential goldmine and will help all future projects I work on, which more than justified the time it took to implement it into the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/playgameservices/play-games-plugin-for-unity" rel="noopener noreferrer"&gt;Google Play Games&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Having the full suite of Google Play Games services in my projects was something I had initially wanted to avoid as I felt it was an excess of bloat that wouldn’t add much value. But while I was playing other mobile games, I noticed they all had chosen to implement the full suite of Google Play Games features including achievements and high scores, which were both things I had planned to implement without using a third party plugin. Having seen these games use the common services utilizing the uniform interface and familiarity that provides made a lot of sense to me and changed my mind on including this. The bloat issue is still something that concerns me and it definitely has a large impact on filesize which isn’t ideal.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://developers.google.com/admob/unity/start" rel="noopener noreferrer"&gt;AdMob&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The inclusion of AdMob was a simple one for me, I wanted to stay in the Google ecosystem of adverts rather than commit to a competitor such as Unity Ads. The plugin offers the facility to play banner ads, rewarded videos and interstitial ads which are all I foresee ever needing in my game development projects. These methods were also linked to other parts of the framework, menus trigger banner ads as an example.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/Unity-UI-Extensions/com.unity.uiextensions" rel="noopener noreferrer"&gt;Unity UI Extensions&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This plugin was something that stood out as providing a lot of value in several ways the key of which being timesaving. Having the ability to create complicated UI systems by simply clicking a button in a drop down and treating them as you would a new GameObject saves an enormous amount of time. These menus again could be tied to other parts of the framework such as the tweening system. Having this connection allows transitions between UI components that add a base level of polish for all projects using the framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="http://dotween.demigiant.com" rel="noopener noreferrer"&gt;DOTween&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Deciding on a tween package to use within the framework was one of my more difficult decisions as there are several which come highly recommended. In the end, I settled on DOTween because it has a professional level of documentation and is very lightweight. With size becoming an issue due to the sheer number of components in the framework packages being lightweight was a big focus. Integrating the tweener with UI Extensions and even Ads was very simple and the effect provided was very good.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.twitter.com/twitterkit/unity/overview" rel="noopener noreferrer"&gt;TwitterKit&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;TwitterKit integration was another package I had initially been reluctant to add, I felt that social sharing in &lt;a href="https://michaelmchambers.co.uk/projects/super-blasty-blasty/" rel="noopener noreferrer"&gt;Super Blasty Blasty&lt;/a&gt; was acceptable and didn’t really warrant a place in the framework. However, after seeing Twitter integration in a few other projects done far more professionally than just opening a browser window, this plugin became an essential inclusion for the framework. Now sharing is done all within the app meaning a user is more likely to stay and play, it also allows callbacks to be performed so sharing score can be rewarded and screenshots can be shared with ease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Game Managers
&lt;/h2&gt;

&lt;p&gt;Having had to write several game managers for various systems in my past I decided it would be best to make them generic and applicable to all situations, this meant following proper design patterns and employing some coding standards that I hadn’t before in past projects. There are 3 main managers; the Game Manager which handles object pooling, tracking score and saving/loading saved player data. The Sound Manager this can be used by all GameObjects to trigger sounds, play the background music, etc. And finally a third party services manager, this was an integration the third party plugins would interface with in order to provide seamless integration with the framework. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Aside from filesize concerns I couldn’t be more happy with how the framework turned out. Working on the Microcosm Framework (named after the new moniker my games will be released under), has rekindled a passion for developing games that had dissipated during the course of 2017. Having a high-quality framework to use in my future and existing projects to take them to the next level technically is exciting and I’m looking forward to seeing the first game that utilizes it in 2018.&lt;/p&gt;

</description>
      <category>admob</category>
      <category>analytics</category>
      <category>csharp</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Microcosm Twitter Bot</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Tue, 13 Oct 2026 18:46:08 +0000</pubDate>
      <link>https://dev.to/mchambers/microcosm-twitter-bot-15d9</link>
      <guid>https://dev.to/mchambers/microcosm-twitter-bot-15d9</guid>
      <description>&lt;p&gt;I set a goal for &lt;a href="https://michaelmchambers.co.uk/30daydev-january/" rel="noopener noreferrer"&gt;#30DayDev January&lt;/a&gt; to create a Twitter bot, there weren’t many requirements beyond that it had to retweet people using the hashtag. I can say that after the first week of January I have achieved that goal and released &lt;a href="https://twitter.com/DevMicrocosm" rel="noopener noreferrer"&gt;DevMicrocosm&lt;/a&gt; into the wild. It followed my rough plan for the project, using Python and &lt;a href="http://docs.tweepy.org/en/v3.5.0/index.html" rel="noopener noreferrer"&gt;Tweepy&lt;/a&gt;. This combination of language and library reaffirmed my expectation that it would be quick to develop and deploy.&lt;/p&gt;

&lt;p&gt;Due to the Python being able to test commands directly in the terminal, it led to some fantastic opportunities to rapidly prototype some ideas, one of these was to use fully fledged configurations for multiple hashtags. This means that there is no need to redeploy the application every time you want to update what hashtags are being retweeted, you can simply change the config file and the next time the python file is run it’ll use the updated settings.&lt;/p&gt;

&lt;p&gt;An example config file for the project can be viewed below. The example demonstrates the simplicity in which multiple hashtags and settings could be defined, I will do an in-depth guide on using the bot in a later post but it is fairly straightforward. These base settings when combined allow for quite a complex behaviour by the bot, leading to better content being shared which in turn produces a higher number of longterm impressions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"twitter_keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"consumer_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"consumer_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"access_token_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"query_objects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"search_query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"#test1"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"tweet_language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"es"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"tweet_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"follow_poster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"user_blacklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"word_blacklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"RT"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"♺"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"day_to_tweet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"search_query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"#test2"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"tweet_language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"tweet_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"follow_poster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"user_blacklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"word_blacklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"RT"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"♺"&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"k"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;"day_to_tweet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"ln"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;lt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;class=&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than reinvent the wheel I opted to fork another repository as a starting point (&lt;a href="https://github.com/basti2342/retweet-bot" rel="noopener noreferrer"&gt;retweet-bot&lt;/a&gt;). I had intended to contribute to this project rather than fork it into my own, however, the original project was no longer being maintained and the extent of the rewrites meant it no longer resembled what it once was, going far beyond the initial projects scope.&lt;/p&gt;

&lt;p&gt;If anybody is interested in viewing the code and contributing to the project I made it open-source and the repository can be found &lt;a href="https://github.com/MChambers1992/retweet-bot" rel="noopener noreferrer"&gt;here.&lt;/a&gt; The readme is slightly out of date but still provides a rough idea on how to run it. I’m looking forward to expanding the bot’s capabilities over the course of the next few months.&lt;/p&gt;

</description>
      <category>microcosmsoftware</category>
      <category>opensource</category>
      <category>python</category>
      <category>twitter</category>
    </item>
    <item>
      <title>Looking Back At Bob’s Space Crusade</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Tue, 13 Oct 2026 18:45:46 +0000</pubDate>
      <link>https://dev.to/mchambers/looking-back-at-bobs-space-crusade-3nc2</link>
      <guid>https://dev.to/mchambers/looking-back-at-bobs-space-crusade-3nc2</guid>
      <description>&lt;p&gt;Bob’s Space Crusade was the first game I ever published, it was released on Kongregate, &lt;a href="https://www.kongregate.com/games/epicchambers/bobs-space-crusade" rel="noopener noreferrer"&gt; on the 30th of December 2011&lt;/a&gt;. It was my first project using Unity3D and it served as a great learning experience in actually getting something out there to be played by the world. It was by no means a masterpiece but the character (who I still have plans to resurrect in future projects) and core mechanic was great. This post will talk about how Bob was originally brought to life and the process that was behind that.&lt;/p&gt;

&lt;p&gt;Going into this project I had a single focus, keep things simple. I didn’t want a spiralling list of features that could never be achieved, I just wanted a core mechanic that was fun and simple. This was for a couple of reasons, the main being I didn’t have enough experience to actually finish a large expansive project. The main goal of the project was to finish it, which I did. In hindsight, I would say the scope was too restrictive, the project in its initial state didn’t play particularly well and would have benefitted greatly from an increased development time.&lt;/p&gt;

&lt;p&gt;The core idea was a game based on the age-old classic; Rock, Paper, Scissors. I was pessimistic that this was the type of mechanic I could base a game on, but after some initial prototyping, it proved to be a pretty solid mechanic. It was very rudimentary which is how it stayed right up until release, but it worked and was fun to play. The game lacked any form of progression or replayability, in fact, the game pretty much remained this single mechanic. It was themed with a space aesthetic with elemental asteroids and a robot, Bob. This theming really helped tie everything together and despite looking basic was probably one of the best aspects of the end product.&lt;/p&gt;

&lt;p&gt;As part of the development process, I managed to port the game to Android devices which was a relatively impressive feat in 2011 for a novice programmer. It featured lacklustre controls on mobile because I had never attempted controls for touch devices prior to this project. This experimentation with the Android platform helped cement my interest in it as a developer, paving the way for future projects like &lt;a href="https://michaelmchambers.co.uk/projects/super-blasty-blasty/" rel="noopener noreferrer"&gt;Super Blasty Blasty&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bob’s Space Crusade provided me with a wealth of knowledge for future projects, lessons learnt during this development process still impact me to this day, 7 years later. The mechanic in this project is one that I would love to try again as I feel there is far more I could do with it now than I could have when this was developed. The main thing I learnt in this project, however, is this, making games is fun. That is a lesson I never want to forget and often need to remind myself of.&lt;/p&gt;

&lt;p&gt;There aren’t any screenshots from during the development process but there is a video showcasing the game in all its glory available on YouTube which I’ve embedded below and of course, the web player is still available &lt;a href="https://www.kongregate.com/games/epicchambers/bobs-space-crusade" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>bobsspacecrusade</category>
      <category>csharp</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Annual Hackathon 2017</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:36:22 +0000</pubDate>
      <link>https://dev.to/mchambers/annual-hackathon-2017-4l35</link>
      <guid>https://dev.to/mchambers/annual-hackathon-2017-4l35</guid>
      <description>&lt;p&gt;Every year I participate in a 48-hour hackathon at work. It’s an opportunity to write software you otherwise wouldn’t get to write, this could be a neat automation tool to save you time on daily tasks or something totally unique like a virtual reality Blackjack game that will never see the light of day. As long as you think you can write it in 2 days nothing is off limits. There are several prize categories; Most Creative Hack, Most Business Value, and Non-Technical Project. This year I decided that I’d give virtual reality a crack and aim to at least get an honourable mention in the Most Creative Hack category.&lt;/p&gt;

&lt;p&gt;The hackathon typically starts on a Thursday and finishes on Friday afternoon before our annual Christmas party. Given its participated in during work time, most teams don’t go hardcore and spend the full 48-hours on their projects like some hackathons you hear about. It is a lot more laid back than that and most hacks usually never have more time spent on it than the 16 working hours, I think this reduced timeframe adds to the pressure of creating something that is worth entering in the contest.&lt;/p&gt;

&lt;p&gt;Thursday morning is generally spent getting a team together and usually writing a brief project plan. My team this year consisted of 3 .NET Developers, a QA Tester and a Product Owner, with varying experience in technologies we could have used to attempt the project we decided it would be best to go with &lt;a href="https://unity3d.com/" rel="noopener noreferrer"&gt;Unity3D&lt;/a&gt; due to the incredibly simple exporting process. Within the team, I had the most experience with Unity having previously released &lt;a href="https://michaelmchambers.co.uk/projects/super-blasty-blasty/" rel="noopener noreferrer"&gt;Super Blasty Blasty&lt;/a&gt; on mobile platforms using the engine and used it extensively during my time at University. Our QA Tester had some previous exposure to the engine as well from a more game designer point of view with a focus on creating 3D scenes, however, the other 2 developers had little to no experience using the engine.&lt;/p&gt;

&lt;p&gt;Our plan, in a nutshell, was to get the basic Blackjack mechanics implemented in VR then integrated into the &lt;a href="https://bedegaming.com/" rel="noopener noreferrer"&gt;Bede Gaming Platform&lt;/a&gt;. This was our main focus during day 1, we attempted to recycle some code we found online to provide us with a basic Blackjack core, however, this ended up creating more work than it got rid of and led to some headaches. To try and split the work efficiently we created a &lt;a href="https://git-scm.com/book/en/v1/Git-Basics-Getting-a-Git-Repository" rel="noopener noreferrer"&gt;Git Repository&lt;/a&gt; so we could all work in parallel and speed up the development, this introduced its own problems as having no experience using Unity in a team we ran into countless issues with Git. Eventually, we found a great &lt;a href="https://robots.thoughtbot.com/how-to-git-with-unity" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; which helped us tackle some of the problems but more often than not we found ourselves using the traditional approach of just dragging files between machines. After we tackled these problems day 1 was rounded out and we had a working Blackjack game, albeit without any virtual reality elements or platform integration.&lt;/p&gt;

&lt;p&gt;Heading into day 2 we decided that making the game function in virtual reality had to be the main goal now we had the game logic handled, we had originally planned this to be done using &lt;a href="https://webvr.info/" rel="noopener noreferrer"&gt;WebVR&lt;/a&gt; and spent several hours trying to do this. Eventually, we gave up and opted for a native Windows application to avoid the headaches of using such an immature piece of software. Making the switch to native app sped our development up massively and got rid of pretty much all the headaches we had been having with &lt;a href="https://webvr.info/" rel="noopener noreferrer"&gt;WebVR&lt;/a&gt;. Unity native VR support ended up being fantastic and in less than an hour after making the switch to native we had the game working in 3d space. Time was running out by this point so we opted to reduce some of platform integration plans and strip out some of the ideas that didn’t add a lot to the overall product. Another one of the features we removed was the ability to play with multiple players in favour of just having the game remain between a single player and the dealer. Stripping these features ultimately gave us more time to polish the core experience and provided a much better outcome for the effort required. Day 2 ended with us pushing the “final” version with about 25 minutes left before the deadline, this version was what we considered our Gold release and it was released into the wild.&lt;/p&gt;

&lt;p&gt;We had the game running for around an hour before official judging began and received pretty much universally positive comments, the features we had left out ended up not being to the detriment of the hack and the end product played very well. When the judges eventually got to try the game I’d hazard a guess that at least 80 hands of Blackjack had been played in our virtual casino. The winner of each category was decided by popular vote once the judges had narrowed the choices down to just 2. Having our game running in the wild for as long as it was ended up paying off, as the 5 of us were crowned overall winners for the Most Creative Hack category, narrowly beating AR Bingo for the top prize.&lt;/p&gt;

&lt;p&gt;tl;dr: Entered hackathon, made cool thing, won a prize.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>htcvive</category>
      <category>oculusrift</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>#30DayDev January</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:36:20 +0000</pubDate>
      <link>https://dev.to/mchambers/30daydev-january-3o0h</link>
      <guid>https://dev.to/mchambers/30daydev-january-3o0h</guid>
      <description>&lt;p&gt;Sometime during 2016, I helped my friend Liam Twose found the hashtag #30DayDev and push it to become of the most widely used game development hashtags on Twitter with a huge number of people actively taking part and even more people seeing the content being developed by the hashtags ever-increasing reach. However, throughout 2017 activity wained as algorithms on hashtags were changed and focus was directed elsewhere into other endeavours. We decided it would be a good thing for the game development community at large to try and revive the hashtag going into the new year and this is what this post will cover, my plan of action for the first #30DayDev of 2018.&lt;/p&gt;

&lt;p&gt;This month will be the first month I’ve gone into with a solid plan of how to tackle #30DayDev pretty much since its inception. I’m not solely focusing on game development this month with some time being dedicated to trying to help revive the hashtag as well. I’m essentially splitting my 30 days into 3 tasks; Twitter Bot, Super Blasty Blasty V2 and knowledge sharing. Between these 3 tasks, I think I should be able to learn a fair few new things as well as produce some quality content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twitter Bot
&lt;/h2&gt;

&lt;p&gt;In order to help the #30DayDev revival I think it’ll need to have some form of cross-polination from other hashtags (#GameDev, #IndieDev, etc). This cross-polination will help it reach the maximum number of eyes and help improve the audience reach. Having a dedicate twitter bot that will retweet and spread content across other relevent hashtags would be a great help.&lt;/p&gt;

&lt;p&gt;I’m planning on writing the bot in Python using &lt;a href="https://www.tweepy.org/" rel="noopener noreferrer"&gt;Tweepy&lt;/a&gt; because I think its the language and library that bests suits the rapid development required to get a project done and deployed in a short time. It’ll also be a chance to write some Python which is a language I don’t often get to write in my personal or professional work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Super Blasty Blasty V2
&lt;/h2&gt;

&lt;p&gt;This project will probably occupy the most of my 30 days as it is by far the most ambitious. I plan on totally ripping the mechanics out of the original &lt;a href="https://michaelmchambers.co.uk/projects/super-blasty-blasty/" rel="noopener noreferrer"&gt;Super Blasty Blasty&lt;/a&gt; and utilizing the new &lt;a href="https://michaelmchambers.co.uk/the-microcosm-framework/" rel="noopener noreferrer"&gt;Microcosm Framework&lt;/a&gt; to enhance the original game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge Sharing
&lt;/h2&gt;

&lt;p&gt;Over time I’ve amassed a huge set of bookmarked articles and videos on game development topics and I’m planning on sharing at least one or two of these every day on Twitter to help spread the knowledge. This won’t take a great deal of time but I feel it could benefit the community a lot. If enough people got involved it could really create a wealth of knowledge being shared using the hashtag.&lt;/p&gt;

</description>
      <category>30daydev</category>
      <category>gamedev</category>
      <category>microcosmframework</category>
      <category>superblastyblasty</category>
    </item>
    <item>
      <title>WordPress Plugin Cross Post for Dev.to: my solution to abandoned and unmaintained plugins</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Tue, 28 Jul 2026 09:58:48 +0000</pubDate>
      <link>https://dev.to/mchambers/wordpress-plugin-cross-post-for-devto-my-solution-to-abandoned-and-unmaintained-plugins-3cac</link>
      <guid>https://dev.to/mchambers/wordpress-plugin-cross-post-for-devto-my-solution-to-abandoned-and-unmaintained-plugins-3cac</guid>
      <description>&lt;p&gt;I’ve released &lt;strong&gt;Cross Post for Dev.to&lt;/strong&gt;, a WordPress plugin that mirrors a post to Dev.to the moment you publish it on your own site. The plugin itself is simple: one hook, one API call, one published mirror. The interesting part of this release isn’t the feature. It’s what had to be true for a zero-dependency plugin to stay trustworthy over time without me babysitting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most small WordPress plugins rot over time. They’re built against whatever WordPress version happens to be current, they lean on a third-party library or two to save time, and neither the plugin nor the library gets revisited until something breaks. Usually a major WordPress release, sometimes a PHP version bump. At that point the maintainer generally abandons the project and so many of these lesser used plugins become useless for users.&lt;/p&gt;

&lt;p&gt;Cross-Post for Dev.to needed to cross-post reliably from WordPress to Dev.to without letting that happen so easily. No bundled third-party SDKs, no external service sitting between the plugin and the Dev.to API, no dependency that could go unmaintained while the plugin using it kept shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why zero-dependency isn’t the whole answer
&lt;/h2&gt;

&lt;p&gt;Removing dependencies fixes one kind of rot but not the other. A plugin with no third-party packages can still quietly break against a new WordPress release; deprecated hooks, changed REST behaviour, PHP version drift. Zero dependencies means less that can fail underneath you; it says nothing about whether the plugin still works today. Those are two different guarantees, and conflating them is how “lightweight” plugins end up just as unmaintained as heavy ones, only with fewer visible warning signs.&lt;/p&gt;

&lt;p&gt;So the actual problem wasn’t “avoid dependencies.” It was “know, continuously and automatically, whether this plugin still works”. Which is a validation problem, not a packaging problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation loop
&lt;/h2&gt;

&lt;p&gt;Two layers of the same idea, one local and one continuous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locally&lt;/strong&gt;, a pre-push hook runs before any commit leaves my machine: PHP linting, PHPCS against the WordPress coding standards, PHPStan for static analysis, then the unit and integration suites. If any of it fails, the push is blocked. This isn’t about catching typos, it’s about making sure a mistake never gets far enough to become someone else’s problem. The cost of finding a broken assumption is lowest the moment it’s introduced; a pre-push hook enforces that you pay it there, not after it is merged and breaking the main branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In CI&lt;/strong&gt;, GitHub Actions runs the same checks against a matrix of PHP and WordPress versions on every push and on a schedule, independent of whether I’ve touched the code recently. That schedule matters more than it looks. WordPress core changes without waiting for plugin authors to notice, and a plugin that hasn’t been touched in eight months can silently stop being compatible with a WordPress release that shipped in month six. Scheduled CI turns “will this still work when someone installs it next week” from a question I’d have to remember to ask into one the pipeline answers on its own.&lt;/p&gt;

&lt;p&gt;The local hook and the CI pipeline check the same things for a reason: local checks exist to keep bad changes from leaving my machine; CI exists to keep bad &lt;em&gt;environments;&lt;/em&gt; a new WordPress version or a new PHP version, from silently invalidating changes that were fine when they were written. Neither one substitutes for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually enables
&lt;/h2&gt;

&lt;p&gt;The real capability this buys isn’t “fewer bugs,” though that’s a side effect. It’s &lt;strong&gt;new WordPress or PHP versions stop being a maintenance crisis&lt;/strong&gt;. Scheduled CI runs the test suite against a matrix of PHP and WordPress versions automatically. When WordPress 6.8 ships or PHP 8.4 goes stable, the pipeline runs the suite the same day. If it passes, great, the plugin is already known to be compatible. If it fails, I see the failure immediately and can fix it or publish a patch before a user even tries to upgrade.&lt;/p&gt;

&lt;p&gt;That’s continuous validation without continuous attention: a plugin only stays trustworthy when someone is actively confirming it still works, and for most maintainers that someone is a person checking manually, usually late. This pipeline makes it an automated process, checking constantly, against the exact environments users will upgrade into, on a schedule that doesn’t depend on my memory or available time.&lt;/p&gt;

&lt;p&gt;The end goal is pragmatic: new WordPress versions and PHP versions don’t impact this plugin in 99% of cases because the pipeline catches them and pushes a new release confirming that. In the remaining 1% of cases where there is a problem I’m notified about, I manually fix and publish a new release. That happens whether I’ve thought about the plugin that week or not.&lt;/p&gt;

&lt;p&gt;That’s the same shift documentation-driven AI development is built on, and it’s worth naming directly: &lt;a href="http://michaelmchambers.co.uk/ai-development-is-a-documentation-problem-not-a-prompting-problem/" rel="noopener noreferrer"&gt;AI development is a documentation problem, not a prompting problem&lt;/a&gt; makes the case that AI coding tools don’t fail because of weak prompts. They fail because the surrounding context (docs, constraints, expected behaviour) isn’t captured anywhere machine-readable. The plugin’s CI pipeline is that same principle applied to compatibility instead of context: don’t rely on someone remembering to check. Encode the check so it runs whether they remember or not.&lt;/p&gt;

&lt;p&gt;The code and pipeline are open: &lt;a href="https://github.com/MChambers1992/cross-post-devto" rel="noopener noreferrer"&gt;github.com/MChambers1992/cross-post-devto&lt;/a&gt;. The plugin itself is on WordPress.org: &lt;a href="https://wordpress.org/plugins/cross-post-for-dev-to/" rel="noopener noreferrer"&gt;wordpress.org/plugins/cross-post-for-dev-to&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>wordpress</category>
      <category>projects</category>
    </item>
    <item>
      <title>AI Development Is a Documentation Problem, Not a Prompting Problem</title>
      <dc:creator>Michael Chambers</dc:creator>
      <pubDate>Wed, 01 Jul 2026 12:28:31 +0000</pubDate>
      <link>https://dev.to/mchambers/ai-development-is-a-documentation-problem-not-a-prompting-problem-1bkb</link>
      <guid>https://dev.to/mchambers/ai-development-is-a-documentation-problem-not-a-prompting-problem-1bkb</guid>
      <description>&lt;p&gt;Most developers start using AI coding tools the same way. They open a chat, describe what they want, and ask it to write the code. Sometimes it works. More often, it produces something almost there but slightly wrong, and fixing it costs more time than it saved.&lt;/p&gt;

&lt;p&gt;That was my experience too, until I changed how I approached using AI in my work. The difference was not the model. It was the method. I have used this approach with both GitHub Copilot and Claude across professional and personal projects, and it has consistently outperformed ad-hoc prompting. What I eventually realised is that I did not have a prompting problem. I had a context management problem. Once I started treating documentation as a shared memory system between me and the AI rather than project paperwork, I immediately noticed the benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem I Kept Running Into
&lt;/h2&gt;

&lt;p&gt;When I asked an AI tool to build something without proper context, I was asking a capable developer to work from a rough verbal description. It would fill gaps with assumptions. The output reflected those assumptions, not my actual requirements.&lt;/p&gt;

&lt;p&gt;The result was code that compiled, looked reasonable, and missed the point.&lt;/p&gt;

&lt;p&gt;There was a second problem compounding that. AI sessions have limits. Context windows fill up, sessions time out, and picking up mid-project means re-explaining everything the tool no longer has access to. Without structure in place, each new session felt like starting from scratch. The tool would drift, contradict earlier decisions, or hallucinate details about code it could no longer see. The problem was not the tool. It was the lack of shared context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Software Engineering Already Knew
&lt;/h2&gt;

&lt;p&gt;Looking back, I was rediscovering something software engineers have understood for decades. Requirements matter. Specifications matter. Shared understanding matters.&lt;/p&gt;

&lt;p&gt;Teams that write architecture decision records, do not do it because they enjoy writing documents. They do it because decisions made without a written record get forgotten, misremembered, or invisibly reversed. The same dynamic applies when working with AI tools, except the stakes are compressed into a single session. An AI tool without a specification is in exactly the same position as a developer joining a project on day one with no documentation and no handover. It will do something, but it will do it based on guesswork rather than intent.&lt;/p&gt;

&lt;p&gt;What AI has not replaced is the need for clarity before execution. It has amplified it. A developer working from ambiguous requirements might ask clarifying questions or flag the ambiguity. An AI tool will proceed confidently and produce something wrong. The principle has not changed. The leverage has. A poorly written specification might slow down a developer. The same ambiguity can send an AI system down an entirely different path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern I Settled On: Plan, Document, Build
&lt;/h2&gt;

&lt;p&gt;Across several projects, I have settled on this consistent three-stage approach. Your mileage may vary, but for me this has improved both output quality and reduce the overall time to produce working code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage one is planning with the AI.&lt;/strong&gt; Before writing a line of code, I use the tool as a thinking partner. I describe the problem, the constraints, what I want to avoid, and what success looks like. This is a conversation, not a prompt. It often takes several exchanges and the plan changes as I think through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage two is producing the shared memory system.&lt;/strong&gt; Once the plan feels solid, I ask the tool to turn it into a structured implementation guide, something detailed enough that a developer, or a completely fresh AI session, could build the project from scratch without clarification. For a download module, that means defining input sources, retry behaviour, storage location, error handling, and expected output contracts. Every integration point written down, every edge case considered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage three is building from it.&lt;/strong&gt; Individual tasks become smaller and more contained. Instead of “build me a pipeline that does X”, each prompt becomes “implement the download module described in section 3 of this guide.” The output is more accurate because the context is explicit rather than inferred.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shared Memory System in Practice
&lt;/h2&gt;

&lt;p&gt;AI tools do not retain memory between sessions. Every time you start a new conversation, the tool has no idea what you built yesterday, what architectural decisions were made, or what constraints were agreed. Without the guide, the first part of every session is reconstruction. With it, you hand the tool the document, point to the relevant section, and pick up exactly where you left off.&lt;/p&gt;

&lt;p&gt;The habit that makes this work is marking progress directly in the guide as you go. Completed sections get marked done. Decisions that shifted during implementation get noted inline. The tool is reading facts rather than guessing at them. In practice this has almost entirely eliminated the drift and hallucination problems I used to run into regularly.&lt;/p&gt;

&lt;p&gt;It also removes the anxiety around session limits. Each session has a bounded task drawn from the guide, and progress is captured before moving on. The next session picks up cleanly from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Habits That Have Helped
&lt;/h2&gt;

&lt;p&gt;Once the implementation guide exists, the prompts themselves become simpler. These habits evolved through trial and error, but they have held up consistently across both tools I have tested this with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference the guide explicitly at the start of every session.&lt;/strong&gt; Provide the relevant section and state clearly what is being implemented. Relying on conversational continuity reduces output quality. Explicit context restores it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One module at a time.&lt;/strong&gt; Bounded tasks produce reliable, testable code. Unbounded prompts invite shortcuts and inconsistencies. The specification defines the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask for tests against the specification, not just the implementation.&lt;/strong&gt; When the guide defines correct behaviour, tests can be written against that definition. This catches a different category of bugs than implementation-only testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the guide as a living document.&lt;/strong&gt; When something changes during implementation, update it immediately. This is the equivalent of keeping ADRs current when a decision is revisited. Without it, the shared memory drifts from reality and stops being reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Approach Is Overkill
&lt;/h2&gt;

&lt;p&gt;Not every task warrants this. For small scripts, quick one-off automation, or narrow prototypes, the overhead of upfront planning outweighs the benefit. Direct prompting works fine and iteration is low cost.&lt;/p&gt;

&lt;p&gt;The pattern pays off on anything with multiple components, anything spanning more than one session, or anything where consistency between modules matters. The more complex the project, the more the absence of a shared memory system costs in drift, rework, and debugging time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Changes
&lt;/h2&gt;

&lt;p&gt;The reality is that most developers are treating AI tools as a prompting problem when they are actually facing a context management problem. Better prompts produce marginal gains. A shared memory system changes the quality ceiling entirely.&lt;/p&gt;

&lt;p&gt;This becomes clear once you experience the difference. The tool stops drifting. Sessions stop feeling like they are fighting against an invisible limit. Output becomes consistent because the inputs are consistent. The specification does not just guide the AI. It eliminates the conditions that produce unreliable output in the first place.&lt;/p&gt;

&lt;p&gt;AI development is not primarily a prompting problem. It is a documentation problem. Treat it accordingly, and the tools available today become significantly more capable than most developers currently experience them to be. That is not a small shift in productivity. For complex projects spanning multiple sessions, it is the difference between a tool that works and one that does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Start
&lt;/h2&gt;

&lt;p&gt;The entry point is low cost. Start a conversation with your AI tool of choice and describe the problem you are solving, not the implementation, the problem. Ask it to produce a structured plan, push back on anything that does not fit, and refine it until it accurately represents what you want to build. Then ask it to turn that plan into a detailed implementation guide that a fresh AI session could execute from scratch.&lt;/p&gt;

&lt;p&gt;That document has been worth more to me than any individual prompt I have written. I have used this pattern with both Copilot and Claude across different project types, and the core approach holds regardless of which tool is being used, because the underlying problem it solves is the same in both cases.&lt;/p&gt;

&lt;p&gt;Improving this one capability, building a shared memory system before building anything else, is one of the highest-leverage changes a developer can make to how they work with AI tools today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
