Spread the love

It’s time for something slightly different. The 19th annual 7 Day Roguelike challenge has started – submissions started 3 March, and continue to 13th March. The goal:

  • March 4th to March 12th in your own timezone: create a complete roguelike game in 7 days, for the 19th Annual 7DRL Challenge!

Note, the focus here is in a complete game. Which is why the goal is to minimise, simplify, and work on what you can finish with your skills in a quicker time. As noted toward the start of the year, I’m excited that my old game development using DirectX on my own engine has moved towards Vulkan through FNA-XNA. As a note, it was after my injury that I decided to move off my old DX11.1 towards Vulkan. While I could get up to rendering in 3D in my own engine around Vulkan itself it didn’t feel as good as I wanted it to, and FNA-XNA does all I want already.

Tileset in use is DawnLike 16×16. Attribution: By DawnBringer. That mad color-bending genius came up with the palette this entire pack lives on. Without his palette, DragonDePlatino would probably never would have even finished DawnHack.

Other parts of the project are attributed to me, ‘edg3’, and my own art added alongside DawnLike for this roguelike is licensed as Freeware.

eVX Addition: Fonts

With that as a note, for eVX which started this year I’ve finally added simple text rendering. There are suggestions of libraries, and I just went ‘let me do it the way I avoided SpriteFont in my XNA days.‘ Without the grey background, this would be what’s used for Arial:

Arial.png

I use Arial for the special symbols primarily; and the ticks, ‘X‘s, and stars, are made using paint.net‘s shapes. For item the game’s text, I am using Roni Siswadi 16, licensed ad freeware:

Roni Siswadi 16

I felt like that would be nice text for my roguelike. I know I can go more in-depth, but I kept it simple. The idea is using a see-through background the characters can be mapped with spacing if they arent monospace:

Excess space around ‘a-zA…’

As you can tell, it’s a simple linear map. This might not be the cleanest way I could have done this, as it renders each character seperately, and it is built to render a single line. I don’t mind that, however, since this challenge is about the roguelike made in 7 days, not my engine, eVX.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace eVX.Structure;

public enum EChar
{
    A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
    CapsA, CapsB, CapsC, CapsD, CapsE, CapsF, CapsG, CapsH, CapsI, CapsJ, CapsK, 
    CapsL, CapsM, CapsN, CapsO, CapsP, CapsQ, CapsR, CapsS, CapsT, CapsU, CapsV, 
    CapsW, CapsX, CapsY, CapsZ,
    N0, N1, N2, N3, N4, N5, N6, N7, N8, N9,
    Grave, Minus, Equals, SquareBracketLeft, SquareBracketRight, SemiColon, Apostrophe, Backslash, Comma, 
    Fullstop, Forwardslash, Tilde, ExlamationMark, At, Hash, Dollar, Percent,
    Caret, Ampersand, Times, CircularBracketLeft, CircularBracketRight, Underscore, Plus, CurlyBracketLeft, 
    CurlyBracketRight, Colon, QuotationMark, Pipe, QuestionMark, GreaterThan, LessThan, 
    Unticked, Ticked, UnX, Xed, Unstar, Starred, 
    C39, C40, C41, C42, C43, C44, C45, C46, C47, C48, C49, C50, C51,
    C52, C53, C54, C55, C56, C57, C58, C59, C60, C61, C62, C63, C64, C65, C66, C67, C68,
    C70, C71, C72, C73, C74, C75, C76, C77, C78, C79, C80, C81, C82, C83, C84, C85, C86,
    C87, C88, C89, C90, C91, C92, C93, C94, C95, C96, C97, C98, C99, C100, C101, C102, C103,
    C104, C105, C106, C107, C108, C109, C110, C111, C112, C113, C114, C115, C116, C117, C118,
    C119, C120, C121, C122, C123, C124, C125, C126, C127, C128, C129, C130, C131, C132, C133,
    C134, C135, C136, C137, // 137 I think?
}

/// <summary>
/// Special Chars e.g. $"{(char)SpecChar.Unticked}{(char)SpecChar.Ticked}{(char)SpecChar.UnX}{(char)SpecChar.Xed}{(char)SpecChar.Unstar}{(char)SpecChar.Starred}"
/// </summary>
public enum SpecChar
{
    Unticked = 'à',
    Ticked = 'á',
    UnX = 'â',
    Xed = 'ã',
    Unstar = 'ä',
    Starred = 'å'
}

public class Texture2DFont : IDisposable
{
    private Texture2D[] _sprites;
    private List<int> _offsets;

    public Texture2DFont(string file, string pixelFile)
    {
        // get the sprite segments
        var subTex = Engine.Content.LoadTex2D("temp", file);
        _sprites = Engine.Content.Tex2DSplit(subTex.Texture2D, 16, 32);

        if (pixelFile != null)
        {
            // get the pixel offsets
            _offsets = new ();
            using var pF = File.OpenRead(pixelFile);
            using var pR = new StreamReader(pF);
            var lines = pR.ReadToEnd().Split("\r\n");
            for (int i = 0; i < lines.Length; ++i)
            {
                _offsets.Add(int.Parse(lines[i]));
            }
        }

        Engine.Content.Unload("temp");
    }

    private Rectangle _drawRect = new Rectangle();
    /// <summary>
    /// Draws left to right; doesnt split lines, etc
    /// </summary>
    /// <param name="height"></param>
    /// <param name="topLeft"></param>
    /// <param name="text"></param>
    public void DrawString(Vector2 topLeft, int height, string text, Color color)
    {
        var hSplit = 16 / (height / 2);
        _drawRect.Height = height;
        _drawRect.Width = height / 2;
        _drawRect.X = (int)topLeft.X;
        _drawRect.Y = (int)topLeft.Y;
        var splt = text.ToCharArray();
        for (int i = 0; i < splt.Length; ++i)
        {
            DrawChar(splt[i], _drawRect, color);
            if (i < splt.Length - 1)
            {
                _drawRect.X += height/2 - (int)(_offsets != null ? GetDiff(splt[i]) * hSplit : 0); // * (height / 16.0)
            }
        }
    }

    private void DrawChar(char c, Rectangle drawRect, Color color)
    {
        // Texture does left half image split
        switch (c)
        {
            case 'a':
                Engine.Content.Draw(_sprites[(int)EChar.A], drawRect, color);
                break;
            // ...
            case 'Z':
                Engine.Content.Draw(_sprites[(int)EChar.CapsZ], drawRect, color);
                break;
            case '0':
                Engine.Content.Draw(_sprites[(int)EChar.N0], drawRect, color);
                break;
            // ...
            case '9':
                Engine.Content.Draw(_sprites[(int)EChar.N9], drawRect, color);
                break;
            case '`':
                Engine.Content.Draw(_sprites[(int)EChar.Grave], drawRect, color);
                break;
            // ...
            case (char)SpecChar.Starred:
                Engine.Content.Draw(_sprites[(int)EChar.Starred], drawRect, color);
                break;
        }
    }

    private int GetDiff(char c)
    {
        if (_offsets == null) return 0;
        try
        {
            switch (c)
            {
                case ' ': return 10; // spaces smaller
                case 'a':
                    return _offsets[(int)EChar.A];
                // ...
                case 'Z':
                    return _offsets[(int)EChar.CapsZ];
                case '0':
                    return _offsets[(int)EChar.N0];
                // ...
                case '9':
                    return _offsets[(int)EChar.N9];
                case '`':
                    return _offsets[(int)EChar.Grave];
                // ...
                case (char)SpecChar.Starred:
                    return _offsets[(int)EChar.Starred];
            }
        }
        catch { }
        return 0;
    }

    public void Dispose()
    {
        if (null != _offsets) _offsets.Clear();
        if (null != _sprites)
        {
            for (int i = _sprites.Length - 1; i >= 0; --i)
            {
                _sprites[i].Dispose();
            }
        }
    }
}

Above code is clipped for quick readability.

A bit clunky, I must say. It does it’s job for my needs.

Example text taken on 6th March

I need to rethink the size adjusment from 16×32 to smaller and larger a bit. The view will be different further in.

Progress: 2023/03/03

First, before I started the roguelike, I got the base eVX project going so that on the 4th I could jump into the game’s actual code. I kept it at the bare minimum, this is much like the eVX sample, bland, boring, nothing to see besides the name of the window changes to NoNameBrandRogue like above.

Prepared window Sample

Next, I took a dive at trying to work out what I wanted to make. Yes, the personal roguelike concept I want to play. I thought of a few small concepts I want to play by the end of the week through the normal start a roguelike from scratch steps:

  • The language: Vulkan through FNA-XNA in C#, using eVX
  • Keep the scope as small as possible

As such, the base steps recommended without the growing scope are in a simple order:

Source: https://youtu.be/jviNpRGuCIU?t=282

With a tiny adjustment for the scope of what I want to create. The ‘saving‘ moved into the first step, and the maps moved towards the second step, only in a simple way. My small plans and goals for this roguelike will help understand why I needed to focus on the save first.

  • An infinite procedural map
  • The character and monster stats, and equipment
    • This includes combat
    • This includes items
    • This includes magic
  • A cool mythical story behind the plot
  • Future: Puzzle actions to add into the world for treasure rooms
  • Future: A way to compress the game’s files (e.g. fonts, textures, sounds) for a smaller package
  • Future: A way to procedurally generate characters (e.g. zombies)
  • Future: A shader for a point to create directional light from the character

I thought of questions around what I wanted as the theme, for a few days, I was going to make a design document. Then I thought about it a little more, and decided I would have a little fun. I asked ChatGPT to answer a few questions, and it lead to my simple design and idea for the infinite world to have a story behind the gameplay. Yep, I’m working on a sort-of design document mapped out by AI, this could be fun. The biggest thing is it helped me take the small, broad, concept and organise a simpler way for me to implement the changes for the game I want to make for this 7DRL.

Yes, indeed. Besides the base project and text rendering – nothing to do with a roguelike’s code, no code added to do with the AI designed concept I want to build as a game. The minimal time available meant it needed to start tomorrow.

If all goes to plan it will be playable with the first 3 major steps sorted soon. The minimal concept which can expand to a better game. The infinite procedural map was the idea I wanted to resolve first since it can help me with the save game structure, that’s why from the image from the YouTube video I had to adjust the minimal steps order.

Progress: 2023/03/04

With it being a Saturday, I spent time on the normal real-world tasks through the day. The focus wasn’t on 7DRL at all, mind you. With that being said, I did get a little progress on the game. I mapped out the Save structure, shown in segments for explanations:

using NoNameBrandRogue.Data.Models;
using SQLite;

namespace NoNameBrandRogue.Data;

public static class Save
{
    private static SQLiteConnection? _db;
    private static Random? _seed;
    public static MapTile[,] Tiles { get; set; } = new MapTile[3, 3]; 
    // Create new save w/ random seed
    public static void OpenConnection(string file)
    {
        if (null != _db)
        {
            _db.Close();
        }

        if (!File.Exists(file)) { File.CreateText(file).Close(); }
        _db = new(file);

        _db.CreateTable<SaveInfo>();
        _db.CreateTable<MapTile>();
        _db.CreateTable<MapTileInner>();

Using nuget, I opted for sqlite-net-pcl, for ease of use. There are 3 data types so far, primarily for seed data, movement data, and the map itself. The ideas was structure it in a way that it can be saved and loaded with relative ease.

        var first = _db.Table<SaveInfo>().FirstOrDefault();
        if (null == first)
        {
            OpenConnectionSeed(new Random().Next(int.MinValue, int.MaxValue));
        }

Next, easily check the save if it contains data – if it doesn’t have any SaveInfo it needs to create a new one with a new seed. I will likely allow people to input seeds as well, but not needed for now.

    public static void OpenConnectionSeed(int seed)
    {
        if (null == _db) throw new Exception("_db shouldn't be null");
        var rand = new Random(seed);
        _db.Insert(new SaveInfo()
        {
            Key = "Seed",
            Value = $"{seed}"
        });
        _db.Insert(new SaveInfo()
        {
            Key = "XMapSeed",
            Value = $"{rand.Next(int.MinValue, int.MaxValue)}"
        });
        _db.Insert(new SaveInfo()
        {
            Key = "YMapSeed",
            Value = $"{rand.Next(int.MinValue, int.MaxValue)}"
        });
        _db.Insert(new SaveInfo()
        {
            Key = "MapTileX",
            Value = "0"
        });
        _db.Insert(new SaveInfo()
        {
            Key = "MapTileY",
            Value = "0"
        });
        _db.SaveTransactionPoint();
        CreateMapTile(true, true, 0, 0, MapType.Empty);
    }

Take note, the seed given creates an XMapSeed, and a YMapSeed, using random generation. This is used for every map tile in this save game to generate the tile. I can make certain tiles generate certain types, currently only MapType.Empty, and have the map tile the player is on with MapTileX, and MapTileY. A simple data oriented approach for creating a new map. The next large step is to make a map tile from scratch:

    /// <summary>
    /// Random Tile Gen comes here for a 32x32 map tile in the world
    ///  - when it creates a tile it checks each of the 8 around it to see if they exist, if not, creates them as well
    /// </summary>
    /// <param name="expands">Does this tile need to expand already?</param>
    /// <param name="isBarren">Tile without enemy chance</param>
    /// <param name="mapTileX">X of where this section of the map is found</param>
    /// <param name="mapTileY">Y of where this section of the map is found</param>
    /// <param name="mapType">proc gen style to use; barren => empty square</param>
    /// <exception cref="Exception"></exception>
    private static void CreateMapTile(bool expands, bool isBarren, int mapTileX, int mapTileY, MapType mapType)
    {
        if (null == _db) throw new Exception($"_db is null, cant create a map tile at {mapTileX},{mapTileY}");
        var XMapSeed = int.Parse(_db.Table<SaveInfo>().Where(it => it.Key == "XMapSeed").First().Value);
        var YMapSeed = int.Parse(_db.Table<SaveInfo>().Where(it => it.Key == "YMapSeed").First().Value);
        var randomSeed = mapTileX * XMapSeed + mapTileY * YMapSeed;
        var mapTileRandom = new Random(randomSeed);

        mapTileX -= 1;
        var leftTile = _db.Table<MapTile>().Where(it => it.X == mapTileX && it.Y == mapTileY).FirstOrDefault();
        mapTileX += 2;
        var rightTile = _db.Table<MapTile>().Where(it => it.X == mapTileX && it.Y == mapTileY).FirstOrDefault();
        mapTileX -= 1;
        mapTileY -= 1;
        var topTile = _db.Table<MapTile>().Where(it => it.X == mapTileX && it.Y == mapTileY).FirstOrDefault();
        mapTileY += 2;
        var bottomTile = _db.Table<MapTile>().Where(it => it.X == mapTileX && it.Y == mapTileY).FirstOrDefault();
        mapTileY -= 1;

        var topdoor = 0;
        var rightdoor = 0;
        var bottomdoor = 0;
        var leftdoor = 0;

        if (null != topTile) topdoor = topTile.BottomDoorX;
        else topdoor = mapTileRandom.Next(1, 30);
        if (null != rightTile) rightdoor = rightTile.LeftDoorY;
        else rightdoor = mapTileRandom.Next(1, 30);
        if (null != bottomTile) bottomdoor = bottomTile.TopDoorX;
        else bottomdoor = mapTileRandom.Next(1, 30);
        if (null != leftTile) leftdoor = leftTile.RightDoorY;
        else leftdoor = mapTileRandom.Next(1, 30);

        var newMapTile = new MapTile()
        {
            X = mapTileX,
            Y = mapTileY,
            LeftDoorY = leftdoor,
            RightDoorY = rightdoor,
            BottomDoorX = bottomdoor,
            TopDoorX = bottomdoor,
            TopDoorX_Locked = mapTileX == 0 && mapTileY == 0 ? false : (mapTileRandom.Next(100) > 74 ? true : false),
            BottomDoorX_Locked = mapTileX == 0 && mapTileY == 0 ? false : (mapTileRandom.Next(100) > 74 ? true : false),
            RightDoorY_Locked = mapTileX == 0 && mapTileY == 0 ? false : (mapTileRandom.Next(100) > 74 ? true : false),
            LeftDoorY_Locked = mapTileX == 0 && mapTileY == 0 ? false : (mapTileRandom.Next(100) > 74 ? true : false),
        };

        _db.Insert(newMapTile);
        _db.SaveTransactionPoint();

With a focus on readability and understanding, it will only generate new tiles when you move between two map tiles. What is shown above just creates the general information a map tile needs. Take note, (0,0)’s map cannot have locked doors. I haven’t decided how that mechanic will even work yet. The other note about the doors, it checks if there is already a tile in each cardinal direction, and if there is already a tile there it uses that doors placement. This means that while it can generate the same map type each time for a tile, it might have slight different procedural outcomes if you take a different path.

The idea here is you can have a slightly different session playing the same seed in a brand new save if you take a different direction. This can lead to different experiences from the same seed, but similar starting points each time.

        switch (mapType)
        {
            case MapType.Empty:
                {
                    // hmm... randomSeed into proc gen functions to start map tiles from scratch - send in newMapTile as well
                    for (int i = 0; i < 32; ++i)
                    {
                        newMapTile.AddInner(0, i, (uint)TileType.Wall1);
                        newMapTile.AddInner(31, i, (uint)TileType.Wall1);
                        newMapTile.AddInner(i, 0, (uint)TileType.Wall1);
                        newMapTile.AddInner(i, 31, (uint)TileType.Wall1);
                    }
                    for (int x = 1; x < 31; ++x)
                    {
                        for (int y = 1; y < 31; ++y)
                        {
                            newMapTile.AddInner(x, y, (uint)TileType.Floor1);
                        }
                    }
                }
                break;
        }

        if (!isBarren)
        {
            // TODO: spawn enemies
        }

        // Generate surrounding tiles
        if (expands)
        {
            for (var x = -1; x <= 1; ++x)
            {
                for (var y = -1; y <= 1; ++y)
                {
                    if (!(x == 0 && y == 0))
                    {
                        int inX = mapTileX + x;
                        int inY = mapTileY + y;
                        var isTile = _db.Table<MapTile>().Where(it => it.X == inX && it.Y == inY).FirstOrDefault();
                        if (null == isTile)
                        {
                            CreateMapTile(false, false, mapTileX + x, mapTileY + y, MapType.Empty);
                        }
                    }
                }
            }
        }
        _db.SaveTransactionPoint();
    }

Next it creates the inner map tile’s inner blocks with procedural generation; currently with the default square. It will then use the floor tiles to place any enemies in the map unless a barren map is generated; this will come in that step. Next, if it is set to expand, it will generate the tiles around it as needed. This expand code will be moved into it’s own function where if you move between map tiles it will be triggered for the tile you move into.

Then OpenConnection finishes with:

        int _x, _y;
        (_x, _y) = GetMapTilePosition();
        for (int x = -1; x <= 1; ++x)
        {
            for (int y = -1; y <= 1; ++y)
            {
                var in_x = x + _x;
                var in_y = y + _y;
                var tile = _db.Table<MapTile>().Where(it => it.X == in_x && it.Y == in_y).FirstOrDefault();
                if (null != tile)
                {
                    tile.LoadInner();
                    Tiles[x + 1, y + 1] = tile;
                }
            }
        }

Get the player’s current map tile position, use it to load the tiles around, their inner tiles, and map it to the Tiles[] array. This approach with the data is there is an easy way to know where a character is in the world, where and item is dropped, and have it loaded by the Map Tile itself with ease. A data oriented, sort of, plan for the world inside this roguelike.

Progress: 2023/03/05

Today’s goal is to get the character in, with it’s stats and saving built in, onto the map with movement between map tiles.

7DRL 2023 Progress

As you can see, I got the movement going. One thing to note, in the 3rd view I noticed where it’s meant to copy a little from the adjascent tile you came from it can accidentally mark it as ‘locked.’ Yeah, locked doors don’t render yet.

It’s been an interesting start; I’m enjoying my use of eVX. I do feel I should swap the art to my own art completely, one day, but the focus this week is to build the roguelike game to enjoy playing. I can clean it up and make it look better once it’s playable.