4
\$\begingroup\$

I am developing a game, its map is based on a 2D city. The city is being generated as string, # are streets, O are buildings.

Here is an example:

#######
#OOOOO#
#OOOOO#
#######
#OOOOO#
#OOOOO#
#######

Now I am replacing the Os with other buildings, by replacing the character with an other. The other buildings are defined in a dictionary, where the identifying character is the key and the value is a number between 1 and 100 which is the propability of the building to replace an O.

Currently I am doing it like this:

// Turn city into charArray
char[] cityChars = renderedCity.ToCharArray();

// Cycle through all KeyValuePairs, ordered by Value ascending
foreach (KeyValuePair<char, byte> kvp in specialBuildings.OrderBy(key => key.Value))
{
    Random r = new Random(seed);
    // Do the following for all normal buildings in the city
    // If a random seeded number is smaller or equal to the propability of the spawn, change the building into the new one
    cityChars = cityChars.Select(c => c == 'O' ? (r.Next(0, 100) <= kvp.Value ? kvp.Key : c) : c).ToArray();
}

Is there a more elegant way to do this? In the best case, this would be achieved by using no loops at all, but I am struggling with LINQ a bit.

\$\endgroup\$
7
  • 3
    \$\begingroup\$ No time to answer atm, but some needs to mention that r should be a private readonly static field, not re-created every iteration. \$\endgroup\$ Aug 12, 2016 at 22:03
  • \$\begingroup\$ I would like to be able to generate the maps with seeds to keep it testable @Mat'sMug. I guess that would not work out if random is a static readonly field \$\endgroup\$
    – Marv
    Aug 12, 2016 at 22:08
  • \$\begingroup\$ Right, I missed the seed part. Still shouldn't be within the loop scope. \$\endgroup\$ Aug 12, 2016 at 22:12
  • \$\begingroup\$ Alright, hopefully a better solution will be given by an answer :) \$\endgroup\$
    – Marv
    Aug 12, 2016 at 22:16
  • \$\begingroup\$ make the probability the dictionary key and the building the value. Yes there will be 100 entries. Then use a trivial dictionary lookup using the random number. \$\endgroup\$
    – radarbob
    Aug 13, 2016 at 1:03

1 Answer 1

2
\$\begingroup\$

I think even the simplest solutions can and should be made modular and SOLID. Let's try to refactor your game to give you an idea how to begin...

We start with a city. A city can have different squares where a square is just a simple character/symbol.

class City : IEnumerable<Square>
{
    private readonly List<Square> _squares = new List<Square>();
    public void Add(Square square)
    {
        _squares.Add(square);
    }
    public IEnumerator<Square> GetEnumerator() => _squares.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Each square has a position and a symbol. It's abstract so we can derive more concrete types from it.

abstract class Square
{
    protected Square(string symbol, int x, int y)
    {
        Symbol = symbol;
        X = x;
        Y = y;
    }
    public string Symbol { get; }
    public int X { get; }
    public int Y { get; }
    public override string ToString() => Symbol;
}

You seem to have two types currently. A building and a street so here they are:

abstract class Square
{
    protected Square(string symbol, int x, int y)
    {
        Symbol = symbol;
        X = x;
        Y = y;
    }
    public string Symbol { get; }
    public int X { get; }
    public int Y { get; }
    public override string ToString() => Symbol;
}

interface IUpgradable 
{ 
    void Upgrade();
}

abstract class Building : Square, IUpgradable    
{
    protected Building(string symbol, int x, int y, int level)
    : base(symbol, x, y)
    {
        Level = level;
    }
    public int Level { get; private set; }
    public void Upgrade()
    {
        Level++;
    }
    public override string ToString() => "O";
}

class School : Building
{
    public School(int x, int y, int level) : base("H", x, y, level) {}
}

class Hospital : Building
{ 
    public Hospital(int x, int y, int level) : base("S", x, y, level) {}
}

class Street : Square
{
    public Street(int x, int y) : base("#", x, y) { }
    public override string ToString() => "#";
}

Buildings can be upgraded thus the Upgrade method which increases its level. Each upgradable type implements the IUpgradable interface then so you can even filer on that.

I don't know how you generate your map but for the sake of this example I did it by hand:

Here's a simple city:

// ###
// #H#
// ###
var city = new City 
{
    new Street(0, 0), new Street(1, 0),new Street(2, 0),
    new Street(0, 1), new Hospital(1, 1),new Street(2, 1),
    new Street(0, 2), new Street(1, 2),new Street(2, 2),
}

Next we want to upgrade some buildings (I might have the condition for upgrading wrong but it should just demostrate the general idea)

Random r = new Random(seed);

// helper encapsulating the can-upgrade logic
var canUpgradeBuilding = new Func<Building, bool>(b => b.Level <= r.Next(0, 100));

// upgrading buildings
foreach (var b in city.OfType<Building>().Where(canUpgradeBuilding))
{
    b.Upgrade();
}
\$\endgroup\$
6
  • \$\begingroup\$ Hey, I am currently generating the empty city as a string like given, it is going to be a template that I construct the actual class with. That gives me the freedom to do changes to a city more easily. So in general your answer definitly helps me to design the city class (actually learnt a ton of stuff which I wondered about before :D) but it does not help me to solve my problem here. Think of a city with only appartments, this shall be filled with schools, hospitals and office-buildings. \$\endgroup\$
    – Marv
    Aug 13, 2016 at 10:04
  • \$\begingroup\$ @Marv this is really simple, if you have more complex buildings then all you need is to make the building abstract and derive more types from it like a hospital or a school. I edited the example. \$\endgroup\$
    – t3chb0t
    Aug 13, 2016 at 10:10
  • \$\begingroup\$ @Marv oh, I overlooked one sentence in you question... you are replacing the buildings and not upgrading them.... \$\endgroup\$
    – t3chb0t
    Aug 13, 2016 at 10:35
  • \$\begingroup\$ A Building is a Square and a Street is a Square. Unintuitive modeling IMHO. I imagine structures occupying squares so a Building/Street has a reference to its location. Also I suspect the OP city grid/map is not fully fleshed out and if it is to evolve it must be decoupled. I'd rather inject a location object than re-write the base class. \$\endgroup\$
    – radarbob
    Aug 13, 2016 at 15:47
  • \$\begingroup\$ @radarbob I'm not saying you're not right but it's just a draft to demonstrate how to start... and what it means to separate or encapsulate functionalities. of course you can create a Point structure, overload the == operator, the GetHashCode, and give each object a location, you can change the list into a hashset and create a custom square/structure-comparer, you can create objects that can be bigger the 1x1 you can do all that stuff... a game cannot be programmed in few minutes especially if all you get are 5 lines of code. You can make all kind of assumptions... \$\endgroup\$
    – t3chb0t
    Aug 13, 2016 at 16:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.