What I'm Up To

Ah ok. I was going to whip up a quick simple game with a simple API where two players would take turns, and they could either move or attack each turn, if the enemy is in an adjacent square. Each tile has one attribute, a defense bonus of either 10, 25, 50, or 100. I was then going to make give a Lua script the ability to take the turns.

It is *not* simple after all. Nor quick. I mean just the game logic. Lua should be trivial at this point.

I'm rapidly approaching 100 lines of code, and there aren't even any player instances yet.

Spoiler "simplegame" code :

I was aiming to pass a "Turn" instance to a Lua environment (or UI). I once had the ambition to also allow a pluggable Lua mod to mod how combat works. But this is getting unsimple quickly.
Code:
using System;

namespace simplegame
{
    class Program
    {
        // public readonly Player Player1;
        // public readonly Player Player2;
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            int Width = 25;
            int Height = 25;
            Map GameMap = new Map(Width, Height);
            Console.WriteLine(GameMap.Tiles[5,5].DefenseBonus);

        }
    }
    class Map
    {
        public readonly int Width;
        public readonly int Height;
        public readonly Tile[,] Tiles;
        public Map(int width = 25, int height = 25)
        {
            Width = width;
            Height = height;
            Tiles = new Tile[Width,Height];
            for(int x=0; x < Width; x++)
            {
                for(int y=0; y < Width; y++)
                {
                    Tiles[x,y] = new Tile();
                }
            }
        }
    }
    class Tile
    {
        public int DefenseBonus{ get; private set; }
        private int[] BonusTable = new int[]{ 10, 25, 50, 100 };
        public Tile()
        {
            DefenseBonus = BonusTable[(new Random()).Next(0,BonusTable.Length)];
        }
    }
    class Player
    {
        public int X;
        public int Y;
        public int Attack;
        public int Defense;
        public readonly string Name;
        public Player(string name, int x, int y, int attack = 1, int defense = 1)
        {
            Name = name;
            X = x;
            Y = y;
            Attack = attack;
            Defense = defense;
        }
    }
    class Turn
    {
        public readonly int TurnNumber;
        private Map GameMap;
        public bool IsTurnDone{ get; private set; }
        public bool IsPlayerDead{ get; private set; }
        public bool IsEnemyDead{ get; private set; }
        public string Action{ get; private set; }
        public string PlayerNote;
        private Player TurnPlayer;
        private Player Enemy;
        public Turn(Map map, Player player, Player enemy, int turnNum)
        {
            TurnPlayer = player;
            GameMap = map;
            Enemy = enemy;
            TurnNumber = turnNum;
            IsPlayerDead = false;
            IsEnemyDead = false;
            IsTurnDone = false;
        }
        public bool IsEnemyInRange{ get => Enemy != null; }
        public bool Attack
        { get {
            if(IsEnemyInRange)
            {
                IsTurnDone = true;
            }
            return false;
        }}
    }
}
 
Well, I have a working "game loop" and "AI". But combat is a coin flip right now, so there's not really any strategy. No UI, just AI only which always attacks if in range, otherwise moves randomly (including possibly "moving" to its current tile). The player AI can't examine the tile defense yet (well, it *can* get the defense of the enemy's tile but not others), but I had intended for it to only have access within 1 tile distance which is also the visible/attack range of the players.

I thought about adding fortification, or perhaps an option to upgrade attack or defense by either passing turns or finding bonuses on the map. But that's going to be a fair bit more code.

And of course I was going to expose the turn object to a Lua environment.

But for now I think I'm done with "simplegame":

Spoiler Sample output :

Code:
Turn 175
  Player 1 moves to 10, 6
  Player 2 moves to 12, 9
Turn 176
  Player 1 moves to 10, 6
  Player 2 moves to 11, 8
Turn 177
  Player 1 moves to 11, 6
  Player 2 moves to 11, 8
Turn 178
  Player 1 moves to 11, 7
  Player 2 dies attacking Player 1
  Player's note: Lame!

Player 1 wins in 179 turns
Tie:
Code:
Turn 249
  Player 1 moves to 19, 15
  Player 2 moves to 21, 20
250 turns with no winner
Win by conquest:
Code:
Turn 54
  Player 1 moves to 3, 6
  Player 2 moves to 5, 7
Turn 55
  Player 1 moves to 4, 7
  Player 2 defeats Player 1
  Player's note: Woo! PWNED!

Player 2 wins in 56 turns


Spoiler simplegame code :
A dotnetcore console app:
Code:
using System;

namespace simplegame
{
    class Program
    {
        static void Main(string[] args)
        {
            int Width = 25;
            int Height = 25;
            int MaxTurns = 250;
            Map GameMap = new Map(Width, Height);

            Player[] Players = new Player[2];
            Players[0] = new Player("Player 1", 5, 5);
            Players[1] = new Player("Player 2", 20, 20);
            Player Winner = null;
            int t;
            for(t=0; Winner==null && t < MaxTurns; t++)
            {
                Console.WriteLine("Turn " + t.ToString());
                for(int p=0; p<Players.Length; p++)
                {
                    // hack assuming only two players
                    int e = 1 - p;
                    Player Enemy = null;
                    if(Math.Abs(Players[p].X - Players[e].X) < 2 && Math.Abs(Players[p].Y - Players[e].Y) < 2)
                    {
                        Enemy = Players[e];
                    }
                    Turn PlayerTurn = new Turn(GameMap, Players[p], Enemy, t);
                    // take turn
                        SimpleAI(PlayerTurn);

                    Console.WriteLine("  " + PlayerTurn.Action);
                    if(PlayerTurn.PlayerNote != null) Console.WriteLine("  Player's note: " + PlayerTurn.PlayerNote);
                    if(PlayerTurn.IsEnemyDead)
                    {
                        Winner = Players[p];
                        break;
                    }
                    if(PlayerTurn.IsPlayerDead)
                    {
                        Winner = Players[e];
                        break;
                    }
                }
            }
            if(Winner!=null)
            {
                Console.WriteLine("\n" + Winner.Name + " wins in " + t.ToString() + (t > 1? " turns\n": " turn\n"));
            }
            else
            {
                Console.WriteLine(t.ToString() + " turns with no winner");
            }
        }
        static void SimpleAI(Turn turn)
        {
            if(turn.IsEnemyInRange)
            {
                bool Result = turn.Attack();
                if(Result)
                {
                    turn.PlayerNote = "Woo! PWNED!";
                }
                else
                {
                    turn.PlayerNote = "Lame!";
                }
            }
            else
            {
                Random Rng = new Random();
                turn.Move(1 - Rng.Next(0,3), 1 - Rng.Next(0,3));
                // turn.PlayerNote = "Where are they?";
            }
        }
    }
    class Map
    {
        public readonly int Width;
        public readonly int Height;
        public readonly Tile[,] Tiles;
        public Map(int width = 25, int height = 25)
        {
            Width = width;
            Height = height;
            Tiles = new Tile[Width,Height];
            for(int x=0; x < Width; x++)
            {
                for(int y=0; y < Width; y++)
                {
                    Tiles[x,y] = new Tile();
                }
            }
        }
    }
    class Tile
    {
        public int DefenseBonus{ get; private set; }
        private int[] BonusTable = new int[]{ 10, 25, 50, 100 };
        public Tile()
        {
            DefenseBonus = BonusTable[(new Random()).Next(0,BonusTable.Length)];
        }
    }
    class Player
    {
        public int X;
        public int Y;
        public int Attack;
        public int Defense;
        public readonly string Name;
        public Player(string name, int x, int y, int attack = 1, int defense = 1)
        {
            Name = name;
            X = x;
            Y = y;
            Attack = attack;
            Defense = defense;
        }
    }
    class Turn
    {
        public readonly int TurnNumber;
        private Map GameMap;
        public bool IsTurnDone{ get; private set; }
        public bool IsPlayerDead{ get; private set; }
        public bool IsEnemyDead{ get; private set; }
        public string Action{ get; private set; }
        public string PlayerNote;
        private Player TurnPlayer;
        private Player Enemy;
        public Turn(Map map, Player player, Player enemy, int turnNum)
        {
            TurnPlayer = player;
            GameMap = map;
            Enemy = enemy;
            TurnNumber = turnNum;
            IsPlayerDead = false;
            IsEnemyDead = false;
            IsTurnDone = false;
        }
        public bool IsEnemyInRange{ get => Enemy != null; }
        public bool Attack()
        {
            if(!IsTurnDone && IsEnemyInRange)
            {
                // Temp coin flip for winner
                bool Win = (new Random()).Next(0,2) == 1;
                if(Win)
                {
                    Action = TurnPlayer.Name + " defeats " + Enemy.Name;
                    IsEnemyDead = true;
                    IsTurnDone = true;
                    return true;
                }
                else
                {
                    Action = TurnPlayer.Name + " dies attacking " + Enemy.Name;
                    IsPlayerDead = true;
                    IsTurnDone = true;
                    return false;
                }
            }
            return false;
        }
        public int EnemyX
        { get{
            if(IsEnemyInRange)
            {
                return Enemy.X;
            }
            return -1;
        }}
        public int EnemyY
        { get{
            if(IsEnemyInRange)
            {
                return Enemy.Y;
            }
            return -1;
        }}
        public int EnemyDefense{ get => GameMap.Tiles[Enemy.X, Enemy.Y].DefenseBonus; }
        public void Move(int x, int y)
        {
            if(!IsTurnDone)
            {
                // Allow only one move point in each axis
                x = x == 0 ? 0 : x / Math.Abs(x);
                y = y == 0 ? 0 : y / Math.Abs(y);
                TurnPlayer.X = Mod((TurnPlayer.X + x), GameMap.Width);
                TurnPlayer.Y = Mod((TurnPlayer.Y + y), GameMap.Height);
                IsTurnDone = true;
                Action = TurnPlayer.Name + " moves to " + TurnPlayer.X.ToString() +", " + TurnPlayer.Y.ToString();
            }
        }
        private int Mod(int n, int m) => ((n % m) + m) % m;
    }
}
 
Last edited:
Ah, this thread is handy. I was looking at my C# repo this morning and wasn't sure where I was. Still not *entirely* sure, but my earlier posts in this thread help.

I wasn't actually motivated to work on C7 per se, but I decided to turn my lua-scriptable sav file reader into an online tool. (For both some YouTube content and trying to build up traffic on a tool site.) I was going to stick with the Go version because it's further along than the C# version and I thought Go made more sense for my online application, but I also want/need to read all the sav file into C# for C7, anyway, and I want to use Lua, and I started thinking of some other tools of interest that might benefit from a graphics library...

So basically I think I'm going to work on the C# sav file reader code and exposing it to Lua for my purposes, but the work will be useful/needed for C7, too.

I seem to have a test script reading some SAV file info, but just looking at the code I'm not even seeing where the data comes from. I guess I'll figure that out first.

This repo is at https://github.com/myjimnelson/read-civ-data . Oh, I had wondered this morning why I was so optimistic as to call this repo "read-civ-data" (not just civ3) and note in the readme I might read other 4X games, but I see now I was thinking the media files, such as from FreeCiv or wherever.

In the bigger picture, family issue did not end well. In unrelated news I've been on a 2.5+month road trip (5450 miles and counting), and I think I'm going to settle down and get a lease for at least a year. I just realized Quintillus, WildWeasel and I have all moved at least once in 2021.
 
Taking some notes here...

It looks like I have at least most of the game sections up to parity with the Go version, but I seem to have just started on the BIC sections. But if I recall they're all fairly trivial lists and should be easy to bring to parity, if boring.

I was briefly horrified at my memory management as each class "copies" the byte array of the data, but C# assigns arrays by reference, so there should be one copy of the raw data in the heap with a bunch of pointers to it, and all the classes are getter/setters based on the raw data and the section byte offset as passed in by the constructor.

That actually works better for my purposes than I have it in Go, I think. I was getting ready to heavily refactor my Go code, but the C# code I think is closer to where I want to be. (Objects with the data available, and Lua can use those as tables, either global or not.)

In my Go version and my early C# prototyping, I have the Lua environment host one game/bic combo, and all the data API is global. But I think what I want going forward is for all lua scripts to be handler functions: pass the function a table of game data, and have it return a table (which in my use case will then be converted to JSON and returned as a result). Or at the very least allow that pattern. The way I have the Go/Lua coded I seem to have it stuck as globals for the moment, but as I say I think the C# code is pretty easy to change around now re: that.


Side note: The class-as-getters paradigm doesn't make adding data and writing out trivial, but that hasn't been my goal so far. And if I/we find need to add something new to a SAV file we'll handle it as one-offs, probably inserting some data and changing a few counters or reference IDs in the process.

Edit: From the premature overthinking department: I probably want to try to be sure that Lua handlers don't return references to the reader API in cases where I want the Lua output to hang around but release the original data from memory. Maybe as simple as a deep copy?
 
Last edited:
A very brief look into IronPython and embedding it vs using Lua:

IronPython can be embedded, but it seems like it may have access to the whole CLR environment and doesn't have a built-in way to isolate it. Whereas Lua is far easier to isolate and was built with that in mind.

So in a very brief look, using Python for scripting is doable, but I'd worry about user-generated scripts more than I would with Lua.

Edit: Random note while reading a reply from Flintlock in the sub bug thread: Oh, what I've been thinking in my head as "trade network IDs" may be bitmaps for the 32 strategic resources; or maybe–and this would make more sense to me–civ IDs. What makes sense is that each tile would have it noted if it is on a valid trade route to each civ. ... But the game also knows about disconnected cities and resources connected to only disconnected-from-capital networks. Well, something to look at more closely sometime in the future.

Edit 2: I started looking at the Lua code and wondered why I'm defining each section individually. Part of it was my original idea is that the Lua environment and globals represented the SAV and BIC, but I'm also hiding functionality of the internal class from Lua.

I think what I want/need is to define an interface or two to limit the methods exposed if I desire. I also want the anti-spoiler code to be defined in QueryCiv3 either as an interface or a child class.

That's going to take some refactoring, but once I do that it should be much easier to implement Lua and IronPython.
 
Last edited:
I'm back where I was earlier in the year: Silverdale, WA, West of Seattle across the Sound. Actually right now I'm in a hotel in Everett, but I have a lease starting the 12th at the same apartment complex I was in before.
 
I think what I want going forward is for all lua scripts to be handler functions: pass the function a table of game data, and have it return a table (which in my use case will then be converted to JSON and returned as a result). Or at the very least allow that pattern.

https://github.com/myjimnelson/read-civ-data/blob/main/examples/LuaHandlerPattern/Program.cs

I got a handler function pattern working, so that's doable. Actually it was easier than expected as MoonSharp seems to handle the data conversions automatically, unlike Go's gopher-lua.

The data extraction is actually not nearly to parity with my Go version, but that's trivial (yet time-consuming) to remedy. I just have the overarching structure in place.

I also sandboxed the Lua environment and refactored the exposed classes so that no file access can be triggered from their methods. That plus a handler function model should allow untrusted Lua code with low risk.

Side note: I wound up using .NET 5 for a test harness and the example. Partially because it was default, and partially because xunit doesn't recognize netstandard2.0 :P . An interesting discovery resulting from that is that the older encoding pages (such as Windows-1252!) have been removed from .NET 5 and have to be loaded via Nuget. So that has to be loaded and registered in .NET 5+ to properly read the Civ3 text data.

One of my bigger short-term challenges is deciding how to split or combine the code. I've split the data reader and file-based utilities into different projects, and the Lua in yet another. So in my little example I needed to add all three plus MoonSharp references and using statements. I could make proxy classes, but that could get confusing quickly.
 
Question: Is there an easy way to add significant transitions between Eras? I recall that someone has been able to implement changing LHs, but how about new Civs arising? Or (better yet) changes to new Tech trees, so that decisions made in one Era far more dramatically change what follows next?
 
In a new-from-scratch game? Sure.

Via Flintlock's patch framework? Maybe, if if if.... His Lua implementation gives me some ideas for triggered events, but while Lua opens the door for user-scripted events, translating the C code manipulation is *at least* as hard as coding it in C in the first place, since the latter is a prerequisite for the former. It would basically be CivIII CTD edition for the most part.

On the other hand, I just had a different idea: *Maybe* the C code could run an external program to manipulate the SAV file directly and then reload the SAV interturn before handing control back to the player? Analyzing and modifying the SAV file seems a lower bar to clear to me than modifying the C patch code. Maybe I'm biased because that's what I've done, and I've seen maybe dozens of people over time knowing at least a little about the SAV file where we have maybe 3 or so who've ever actually produced results with the C or C++ code.
 
For what it's worth, I went back to the "simple game" project from a few months ago and added Lua "AI". https://github.com/C7-Game/SimpleGame

It's the pattern I have in mind for user- (or mod-) created scripts. The Lua defines a handler function and expects an object with methods and members. The C# program defines the object with untrusted user exposure in mind. Or trusted user, even, if it's for game mod or game admin access, but still sandboxed from e.g. deleting the user's filesystem or downloading viruses.

One thing I need to test is setting members and setters from Lua back into the C# object. That may be an issue with by-value members/setters or even with otherwise by-reference types that have to be type-translated between Lua and C#.

I guess this is mostly for @WildWeazel , but I'm not sure how easy to read "simplegame" is as it got less simple codewise every commit.

But here's an example Lua "AI" turn handler:

Code:
function player_turn(turn)
    if turn.isEnemyInRange == true then
        turn.attack()
    else
        local x = math.random(-1,1)
        local y = math.random(-1,1)
        turn.move(x,y)
    end
end

Dev notes:
  • Turn is a class defined in Turn.cs and is an available data type in the Lua environment
  • In Lua, Pascal case members appear as camel case
  • IsEnemyInRange is a public getter with private setter, and that works as expected
  • The Move method crashed when I passed math.random(-1,1) as parameters, so I had to assign Lua vars then pass them. I imagine Lua tried to pass a Lua function reference back to the Turn instance the first way.
  • So Lua is very useful and fairly easy, but there are little tripping points like data conversions and names
  • As implemented in simplegame, each AI function persists in its own Lua environment, so if it wanted to store historical positions and such in globals then it could learn as it plays, at least in the runtime of one game.
 
  • As implemented in simplegame, each AI function persists in its own Lua environment, so if it wanted to store historical positions and such in globals then it could learn as it plays, at least in the runtime of one game.

It just randomly occurred to me that persisting data in the Lua environment is a bad pattern for C7 as it only persists while the code is loaded. I'm now thinking we might explicitly run every handler in a clean Lua environment to ensure consistent behavior. Unless it's a looped batch process like interturn or other triggered event.

If we want any script to have any persistent data we should allow for it in the native class instance and ensure it goes in the save file. Or config file, whatever the context of the script is.

As I've mentioned before, I can imagine many contexts for scripts, and ideally any pluggable functionality should also be scriptable.
 
I'm not sure where these ideas should go, but I'll put them in my thread so I can better find them later.

Some overnight sleepytime thinking: Separation is needed between Civ3 constructs and C7 constructs. So I think I see a clear data conversion pattern path now: One subproject will read Civ3 data, and either that same project or an intermediate one converts the data to JSON which seems to be the likely C7 serialized data format.

It also hit me that I may make the conversion in Lua. That seemed to make a lot more sense while I was in bed, but basically I've recently realized on multiple projects that a Lua data structure to JSON is pretty straightforward, and there's already a library for it, so having the C# code expose Civ3 data in a Lua-accessible format allows several things, and I may as well just use Lua for it all. It's a one-shot conversion process per import, anyway.

I also started thinking about how difficult it will quickly become to manage all the Lua scripts as I currently want to have everything Lua-extensible. But I think the answer is easy: Lua snippets in JSON.

I had been hung up on the differences between handing a script a global environment and using a handler pattern, and I guess that might make a difference if there is output, but I'm realizing our use case is mostly exposing a task-appropriate C# class/API to Lua (or C# libraries, or maybe IronPython) and having them use the levers on the object to make stuff happen in the C# code. As long as that's the case, a script snippet doesn't need to have much context, it just needs to know the var name to manipulate.

Which means the script is simple and could easily be stuffed into JSON. I mean surely there would be an editor/injector, but with script in JSON we wouldn't need to manage a directory full of .lua files.

How that might look:

Code:
{
  "name" : thing,
  "lua": {
    "override" : "civ3.move(1,1)",
    "pre" : "uh some lua here",
    "post" : "yeah more lua here"
  },
  "otherValues" : "etc., etc."
}

So there is probably a default action in every scriptable context, so the override script might replace the default action if present. And maybe there are Lua-accessible routines to call, e.g. "pass the task object the more aggressive predefined AI" or a custom-coded script, or a hybrid of both, because Lua.

For contexts where you just want to jump in and change something before or after the default scriptable action, "pre" or "post" scripts will run before or after the default action is run.

So basically I am currently imagining every pluggable API to be exposed by a class interface and actions optionally modifiable by Lua.

But that's just some late-night half-asleep thinking and ideation. Who knows, maybe this will sound dumb after some further thought or just impractical.


However, for at least the Civ3 SAV and BIC imports I think I'm set on having Lua pull the data and output JSON objects be the target flow.


Graphics, though, probably not so much. I'd like to have separation between the native C7 format and Civ3's pcx and flc, but the logic to read the media files doesn't really belong in the main C7 came code, and bulk-converting all the Civ3 assets to a "C7 format" just wastes a lot of disk space for no real gain. Maybe the answer there is an interface-based map draw-er and an intermediate library.
 
So I'm digging around in the C7 Prototype repo and refamiliarizing myself with parts of it.

Oh, my SAV reader and display-er is already in the repo! And working. It's just not hooked up to the current main menu. In the FileSystem widget, open the TempTiles folder, double-click the TempTiles.tscn , and then run scene (not project). It has scrolling and zooming working, and a little top-bar UI where you can open a file which loads the map. (Base terrain only, and fully revealed/spoiled for now.)

I may try to hook it up to the main menu.

Edit: Ok, I think I'm going to just move TempTiles into C7/UtilScenes because it's really handy for trying to figure out map tile data as when you set the debug offset, that offset value appears on each tile. But I'll also merge it into the C7Game scene.
 
Last edited:
Back
Top Bottom