I like using Twilio Voice and SMS APIs to develop voice and text-based applications. I thought an SMS-based game would be fun to implement.

Prerequisites

If you want to follow along, you will need the following:

Setting Up the Project

First, start by creating an ASP.NET Core Web API project:

mkdir HighLowNumbergame
cd HighLowNumbergame
dotnet new webapi

You’ll use Twilio SDK, so add that NuGet package along with ASP.NET helpers to the project:

dotnet add package Twilio
dotnet add package Twilio.AspNet.Core

Then, add a controller named IncomingSmsController and update its contents as below:

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
using Twilio.TwiML;

namespace HighLowNumbergame.Controllers;
 
[ApiController]
[Route("[controller]")]
public class IncomingSmsController : TwilioController
{    
    [HttpPost]
    public async Task<TwiMLResult> Index()
    {
        var messagingResponse = new MessagingResponse();
        messagingResponse.Message("Testing...");
        return TwiML(messagingResponse);
    }
}

Then, run your application:

dotnet run

You should see the HTTP port that your application is listening on (e.g http://localhost:5004)

Next, run ngrok and redirect to your local URL such as :

ngrok http http://localhost:5004

This will generate a public URL for you and tunnel all your requests to your local host.

The final step to receiving SMS is to inform Twilio that we accept incoming SMS at the public URL that ngrok gave.

Go to Twilio Console, select your number and update the A Message Comes In section to webhook. Set the webhook URL to your ngrok URL followed by /IncomingSMS, for example, https://{some random number}.eu.ngrok.io/IncomingSms.

Now when you send an SMS to that number, your API will receive the message. Once you’ve tested and received an SMS response saying “Testing…” you can move on to the game implementation.

Implementing the Game

The game logic is simple. The user is expected either the commands play (starts a new game), exit (ends the existing game) or a number between 1 and 100.

The key to the implementation is state management. As you can imagine, normally, your application receives a new SMS every time. You can, of course, choose to store the state in a database, but for small amounts of data, you can use cookies.

You can use cookies with Twilio SMS just like you would normally use in a web application. They chose not to reinvent the wheel and stuck with the web standards (You can read more on Twilio cookies here).

In this implementation, it’s used like this:

if (userMessage == "play")
{
    currentGame = new Game();
    responseMessage = $"Welcome to number guessing game. Send your guesses between {Constants.MIN_NUMBER} and {Constants.MAX_NUMBER}";
    Response.Cookies.Append("GAME_DATA", JsonConvert.SerializeObject(currentGame));
}

The Game class just stores the target and guess count:

namespace HighLowNumbergame;

public class Game
{
    public int Target { get; set; } = new Random((int)DateTime.UtcNow.Ticks).Next(Constants.MIN_NUMBER, Constants.MAX_NUMBER + 1); // Ceiling is exclusive so add 1
    public int GuessCount { get; set; } = 0;
}

and the minimum and maximum numbers are defined in a file named Constants.cs:

public class Constants
{
    public const int MIN_NUMBER = 1;
    public const int MAX_NUMBER = 100;
}

Final step: Replace the controller code with the code below:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Twilio.AspNet.Core;
using Twilio.TwiML;

namespace HighLowNumbergame.Controllers;
 
[ApiController]
[Route("[controller]")]
public class IncomingSmsController : TwilioController
{
    Game currentGame;
    bool CHEAT_MODE = true;
    
    [HttpPost]
    public async Task<TwiMLResult> Index()
    {
        var form = await Request.ReadFormAsync();

        currentGame = ResumeOrCreateGame();
        
        var userMessage = form["Body"].ToString().Trim().ToLowerInvariant();
        var responseMessage = string.Empty;
        
        if (userMessage == "play")
        {
            currentGame = new Game();
            responseMessage = $"Welcome to number guessing game. Send your guesses between {Constants.MIN_NUMBER} and {Constants.MAX_NUMBER}";
            Response.Cookies.Append("GAME_DATA", JsonConvert.SerializeObject(currentGame));
        }
        else if (userMessage == "exit")
        {
            if (currentGame == null)
            {
                responseMessage = "No game in progress";
            }
            else
            {
                responseMessage = $"Quiting game. The target was {currentGame.Target}. You guesses {currentGame.GuessCount} times. Better luck next time!";
                Response.Cookies.Delete("GAME_DATA");                
            }
        }
        else if (int.TryParse(userMessage, out int guessedNumber))
        {
            if (currentGame == null)
            {
                responseMessage = "No game in progress";
            }
            else
            {
                if (guessedNumber < Constants.MIN_NUMBER || guessedNumber > Constants.MAX_NUMBER)
                {
                    responseMessage = $"Please guess between {Constants.MIN_NUMBER} and {Constants.MAX_NUMBER}";
                }
                else if (guessedNumber == currentGame.Target)
                {
                    currentGame.GuessCount++;
                    responseMessage = $"Congratulations!. You've guessed correctly in {currentGame.GuessCount} guesses.";
                    Response.Cookies.Delete("GAME_DATA");
                }
                else
                {
                    currentGame.GuessCount++;
                    
                    if (guessedNumber > currentGame.Target)
                    {
                        responseMessage = "Too high!";
                    }
                    else if (guessedNumber < currentGame.Target)
                    {
                        responseMessage = "Too low!";
                    }

                    Response.Cookies.Append("GAME_DATA", JsonConvert.SerializeObject(currentGame));
                }
            }
        }
        else
        {
            responseMessage = "Unknown command";
        }
        
        var messagingResponse = new MessagingResponse();

        if (CHEAT_MODE && currentGame != null)
        {
            responseMessage = $"{responseMessage}\n{JsonConvert.SerializeObject(currentGame)}";
        }

        messagingResponse.Message(responseMessage);
        return TwiML(messagingResponse);
    }

    private Game ResumeOrCreateGame()
    {
        var cookies = Request.Cookies;
        if (cookies.TryGetValue("GAME_DATA", out string rawGameJson))
        {
            return JsonConvert.DeserializeObject<Game>(rawGameJson);
        }

        return new Game();
    }
}

Now, run the application again, and you should be able to play the game. By default, I added a cheat mode flag that appends the game object to the output messages. This helps with debugging and testing.

Conclusion

In this post, I shared a small project I developed with Twilio SMS API. It was a fun little project, and I hope you enjoyed it as well. If you would like to get the full source code, you can clone my repository.


Volkan Paksoy

Volkan Paksoy is a software developer with more than 15 years of experience, focusing mostly on C# and AWS. He’s a home lab and self-hosting fan who loves to spend his personal time developing hobby projects with Raspberry Pi, Arduino, LEGO and everything in-between.