My Code Quality – Roguelike Design part 3


In the previous article, I shared some code showing how I’m storing and retrieving things from the database. I want to clear up some things before we move on. In future articles we’ll look at the steps I took to be able to generate weapons of various “level” even though we aren’t using level in my game engine.

Retrieving Attributes

All the various parts of my weapon name are stored in separate mySQL tables with identical structure. Default values are set for all of these tables to simplify the code that generates the final weapon values. The tables we use are:

  • weapon_type
  • weapon_element
  • weapon_prefix
  • weapon_adjective
  • weapon_noun

I’m using a basic SQL command to return ALL of the columns of the table. Here’s what I shared in part 2

  // prefix
  if ((rand(0, 100) * $level)  >= 75) {   // 25% of all weapons
    $sql = $pdo->query("SELECT * FROM weapon_prefix WHERE active = 1 ORDER BY RAND() LIMIT 1");
    $weapon_prefix = $sql->fetchAll(PDO::FETCH_ASSOC);
    $weapon = sumWeaponAttributes($weapon, $weapon_prefix[0]);
    $weapon['str'] += 3;
  }

I didn’t fully explain what this was doing so that I could get to the point of that post, but there are a few things to discuss.

First, we are only selecting from entries that have an active value of 1. This means I can turn some things off in the database if they become a problem down the road and not have to touch my code. I also hinted at letting people submit their own attributes to expand my dataset and this is how I’d achieve that without having to worry about offensive terms showing up un-vetted.

Second, I am using the SQL RAND() function to return a random row. This function isn’t cryptographically secure, but it should be good enough for our needs… especially considering all these SELECT statements are already under a layer of PHP rand() to begin with.

Third, in most people’s opinion (including mine) SELECT * is bad. It’s always better to be explicit. However, I’m not using a framework for this engine. That means there is no database abstraction layer I can rely on to keep my code from doing something it shouldn’t. BUT, I want to be able to change my database structure down the road without having to find all the instances in the code where I touch a table. This is a combination of my own lazies for the sake of “getting it done” and “reinventing the wheel.”

It’s going to be very important going forward that you don’t take anything I share in this post or in future posts as The Word. I’m just messing around, and hopefully it is obvious to the outside observer.

I don’t want to continue writing disclaimers about the code you are going to see. There will be a veritable dump-ton of bad practice presented here. If that bothers you, please don’t hesitate to shut up about it and never tell me.

That being said, if you have constructive feedback, I’m all ears! I am always eager to learn new ways of doing things. I’m studying up on functional code right now. I’d be more than happy to entertain your ideas about how I could transform some of this to a more functional style.

Case In Point

I also shared this little bit last post:

function sumWeaponAttributes($weapon, $array) {
  foreach($array as $key=>$value) {
    if (array_key_exists($key, $weapon)) {
      $weapon[$key] += $value;
    }
  }
  return $weapon;
}

Wow is that a hot mess that’s sure to ruffle some tailfeathers. What’s going on here? Well, I take an array for my weapon $weapon[] and my array for the new attribute $array[] and I loop through my weapon array and check to see if each key exists in the attribute array. If it does, we sum the two values. Easy enough.

This is one of those examples of PHP “magic” that people point to and laugh. Have you identified the problem yet? I’m just magically assuming that the values in my arrays are some form of number… INT/DOUBLE/FLOAT etc.

I said before that the tables were all identical. So, what happens when we get to the word column that contains the plain text name of the attribute? You can’t concatenate strings in PHP with the += operator. That’s used for numerical expressions only… so… PHP just ignores it. It silently throws a “warning” and just keeps chugging along.

Yes, this is messy. Yes, this is bad practice. Does it work? It sure does. could we make it better… sure… and I have. Here’s the newer, more complete function:

function sumWeaponAttributes($weapon, $array) {
  foreach($array as $key=>$value) {
    if (array_key_exists($key, $weapon) && is_numeric($value)) {
      $weapon[$key] += $value;
    }
  }

  $weapon['name'] .= ucwords(strtolower($array['word'].' ')) ;
  return $weapon;
}

Now we at least try to check if the value is a number before we go adding it up. I say try because it doesn’t quite work that way. Again, there is potential for PHP “magic” to get in our way with is_numeric().

php> $value = '1';
php> echo is_numeric($value);
1

So, what just happened? Well, I declared $value as a string ‘1’ and PHP thinks it’s a number. That’s what happened. This “problem” is rampant all throughout PHP because it’s trying to guess what you mean, instead of forcing you to be explicit. If you try to do math on a string, PHP will do it’s best to turn that string into a number for you… and sometimes I don’t mind it… but ooh boy, you publish something like this on the internet and the LOLPHP crew will crawl out of the woodwork to tell you that your code is garbage and you should use a “real” language.

The Whole Picture

Next post, we are going to work towards adjusting our “randomness” so that we can ask the generator to make more or less powerful weapons. I did some laughable stuff at first to get that working and I’m excited to talk about it.

Before we do that though, and now that I’ve got all my complaining about code-haters out of the way, here is the full function (functions actually) I had at this time to generate a weapon.

////////////////////////////////////////////////////////////////////////////////
// fuction createWeapon()
////////////////////////////////////////////////////////////////////////////////
// Creates a fantasy weapon for the game No Rogues Allowed
// takes no arguments, returns associative array with:
// name       => str   - full name of the weapon in ucase format
// hands      => int   - how many hands needed to use this weapon
// multiplier => float - modifier for all rolls
// attack     => int   - base attack
// defense    => int   - base defense
// magic      => int   - base magic
// flaming    => float - attack modifier vs enemies weak to fire
// freezing   => float - attack modifier vs enemies weak to ice
// shocking   => float - attack modifier vs enemies weak to electricity
// holy       => float - attack modifier vs enemies weak to holy
// evil       => float - attack modifier vs enemies weak to evil
// sneak      => float - modifier to players ability to flea combat
// crit       => float - crit chance modifier
////////////////////////////////////////////////////////////////////////////////
function createWeapon() {
  global $pdo;

  // initialize array to hold our weapon stats
  $weapon = [
    'name'=>'',
    'hands'=>0,
    'multiplier'=>0.0,
    'attack'=>0,
    'defense'=>0,
    'magic'=>0,
    'flaming'=>0.0,
    'freezing'=>0.0,
    'shocking'=>0.0,
    'holy'=>0.0,
    'evil'=>0.0,
    'sneak'=>0.0,
    'crit'=>0.0
  ];

  // Element
  if (rand(0, 100) >= 95) {   // 5% of all weapons
    // a very small percent will be crippling
    if (rand(0, 100)  >= 99) {  // 1% of the 5% with elements
      $sql = $pdo->query("SELECT * FROM weapon_element WHERE word = 'crippling'");
      $weapon_element = $sql->fetchAll(PDO::FETCH_ASSOC);
      $weapon = sumWeaponAttributes($weapon, $weapon_element[0]);
    } else {
      $sql = $pdo->query("SELECT * FROM weapon_element WHERE active = 1 ORDER BY RAND() LIMIT 1");
      $weapon_element_1 = $sql->fetchAll(PDO::FETCH_ASSOC);
      $weapon = sumWeaponAttributes($weapon, $weapon_element_1[0]);

      // some weapons may have two elements
      if (rand(0, 100)  >= 98) {  // 3% of the 5% with elements receive a second
        $weapon['name'] .= '& ';
        $sql = $pdo->query("SELECT * FROM weapon_element WHERE active = 1 AND id != ".$weapon_element_1[0]['id']. " ORDER BY RAND() LIMIT 1");
        $weapon_element_2 = $sql->fetchAll(PDO::FETCH_ASSOC);
        $weapon = sumWeaponAttributes($weapon, $weapon_element_2[0]);
      }
    }
  }

  // prefix
  if (rand(0, 100)  >= 75) {   // 25% of all weapons
    $sql = $pdo->query("SELECT * FROM weapon_prefix WHERE active = 1 ORDER BY RAND() LIMIT 1");
    $weapon_prefix = $sql->fetchAll(PDO::FETCH_ASSOC);
    $weapon = sumWeaponAttributes($weapon, $weapon_prefix[0]);
  }

  // weapon
  $sql = $pdo->query("SELECT * FROM weapon_type WHERE active = 1 ORDER BY RAND() LIMIT 1");
  $weapon_type = $sql->fetchAll(PDO::FETCH_ASSOC);
  $weapon = sumWeaponAttributes($weapon, $weapon_type[0]);
  // weapons might be better or worse than normal condition.
  $condition = rand(0, round($weapon['attack'] * .4));
  if(rand(0, 100)  >= 98) {
    // some weapons might be amazing
    $weapon['attack'] = $weapon['attack'] * 2;
  } else if(rand(0, 100)  > 38) {
    $weapon['attack'] = $weapon['attack'] + $condition;
  } else {
    $weapon['attack'] = $weapon['attack'] - $condition;
  }

  // attribute and possibly adjective
  if (rand(0, 100)  >= 65) {   // 35% of all weapons receive an attribute
    $weapon['name'] .= 'of ';

    if (rand(0, 100)  >= 60) {   // 40% of the 35% receive an adjective
      $sql = $pdo->query("SELECT * FROM weapon_adjective WHERE active = 1 ORDER BY RAND() LIMIT 1");
      $weapon_adjective = $sql->fetchAll(PDO::FETCH_ASSOC);
      $weapon = sumWeaponAttributes($weapon, $weapon_adjective[0]);
    }
    $sql = $pdo->query("SELECT * FROM weapon_abstract WHERE active = 1 ORDER BY RAND() LIMIT 1");
    $weapon_abstract = $sql->fetchAll(PDO::FETCH_ASSOC);
    $weapon = sumWeaponAttributes($weapon, $weapon_abstract[0]);
  }

  // power modifier
  if (rand(0, 100)  >= 90) {
    $modifier = rand(1, 100);

    switch (true) {
      case ($modifier >= 98):
        $weapon['name'] .= '+5';
        $weapon['multiplier'] += 1;
        break;

      case ($modifier >= 92):
        $weapon['name'] .= '+4';
        $weapon['multiplier'] += .8;
        break;

      case ($modifier >= 85):
        $weapon['name'] .= '+3';
        $weapon['multiplier'] += .6;
        break;

      case ($modifier >= 70):
        $weapon['name'] .= '+2';
        $weapon['multiplier'] += .4;
        break;

      default:
        $weapon['name'] .= '+1';
        $weapon['multiplier'] += .2;
    }
  }
  $weapon['name'] = trim($weapon['name']); // strip trailing spaces
  if($weapon['hands'] < 1) $weapon['hands'] = 1;  // can't have a zero-handed weapon
  if($weapon['hands'] > 2) $weapon['hands'] = 2;  // can't use more than 2 hands
  return $weapon;
}

////////////////////////////////////////////////////////////////////////////////
// fuction sumWeaponAttributes()
////////////////////////////////////////////////////////////////////////////////
// Adds attributes for a weapon
// takes two associative arrays, sums their contents based on key
// returns associative array
////////////////////////////////////////////////////////////////////////////////
function sumWeaponAttributes($weapon, $array) {
  foreach($array as $key=>$value) {
    if (array_key_exists($key, $weapon) && is_numeric($value)) {
      $weapon[$key] += $value;
    }
  }

  $weapon['name'] .= ucwords(strtolower($array['word'].' ')) ;
  return $weapon;
}

There’s a few things hiding in there that we haven’t talked about yet, and maybe we will some day if I ever get around to the “game” part of this experiment. For now, I’ll leave those little things in there for you to find on your own. I comment a lot. I like to comment all the things. Future me likes this. You should write more comments too. Like, below this post. Let me know what you think. Until next time!

https://noroguesallowed.com

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.