Data-driven and Magic Numbers

posted in Event-driven engine for project Hangman
Published August 04, 2021
Advertisement

In my opinion, Magic Numbers ("Unique values with unexplained meaning") are a red flag in your code. They mean that you are mixing data and code. You are defining the details of the game behavior in your code and usually you need to tweak them a lot.

Imaging for example that you hard code the position of the different elements in the screen, the font size, the margins, … Whenever you change the screen resolution, you will need to modify them. Even if you use relative positions, when you add a new element, you need to rearrange all the elements. Whenever it is possible, you must delay setting those details creating an easy to modify configuration.

We can see some examples in the hangman main class.
It's not only numbers, they are strings, and other initialized variables.

package manager;

import components.Image;
import components.ImageAtlas;
import gateways.IOFactory;
import hangman.Play;

public class Hangman {

	public static void main(String[] args) {

		IIOFactory factory = new IOFactory();
		IKeyboardInput in = factory.makeCanvasAWTInput();	
		IScreenOutput out = factory.makeScreenCanvasAWTOutput();		
		IStorage resource = factory.makeResourceStorage();

		int attempts = 7;
		
		ImageAtlas imgAtlas = resource.getImageAtlas("hangman_atlas.png", 4, 4);
		imgAtlas.setIndex(7 - attempts);
		Image img = imgAtlas;
		
		int row = 0;
		out.printLine(0, row++, "HANGMAN. New Play.");
		
		Play hangman = new Play(attempts);
		
		String word = hangman.getCurrentRevealedWord('_');		
		out.printLine(0, row++, ">>> " + word + " <<<");
		out.printImage(img, 0.50f, 1.00f, 0.00f, 0.50f);
		
		// 2. Game loop
		boolean success = false;
		while (attempts > 0 && !success) {
			
			// 2.1 User input
			char mchar = in.readChar();
			
			// 2.2 Logic check
			boolean result = hangman.guess(mchar);
			
			// 2.3 Win/Lose check
			success = hangman.isWordCompletedRevealed();
			
			// 2.4 Output
			if ( true == result ) {
				word = hangman.getCurrentRevealedWord('_');
				out.printLine(0, row++, ">>> " + word + " <<<");
				if ( success ) {
					out.printLine(0, row++, "YOU WIN");
				}
			} else {
				attempts--;
				out.printLine(0, row++, "no " + mchar + ", try again. " + attempts + " left.");
				if ( attempts <= 0 ) {
					out.printLine(0, row++, "You lose. It was " + hangman.getSecretWord());
				}
				imgAtlas.setIndex(7 - attempts);
				out.printImage(img, 0.50f, 1.00f, 0.00f, 0.50f);
			}
		}
		
		// 3. Game close
		in.readChar();
		System.exit(0);
		
	}
}

What mean those numbers in these lines of code?

int attempts = 7;
int row = 0;
out.printLine(0, row++, "HANGMAN. New Play.");
out.printImage(img, 0.50f, 1.00f, 0.00f, 0.50f);

You can image the meaning of some of them, but even I will forget their meaning in a couple of days.
I can create longer variable names, to easily identify their use but they will still be all over the code.
There are simple solutions:

  • Global variables.
  • Singleton class with all the configuration variables.
  • Blackboards.
  • Object with all the data shared by argument will all the other classes.
  • Others (please, share in the comments)

But they are still data in the code, and they create hard dependencies . All your classes need to know about that global class. And all the data is placed together in the same place. But it is better than having them spread in your code.

In the previous post (https://www.gamedev.net/blogs/entry/2272177-data-driven-and-components/​)​ I talked about the Entity-Component. All the actors in our game engine will be entities with components that will store the needed data. So in this situation, all the data will be stored in the plain text files that will create the classes. The data will be grouped by component.
Just we move the magic number and data to the entity-component that will need it in their definition.

For example, to draw an image atlas, I need the x, y, width, height (Transformation) and the ImageAtlas (picture with small pictures inside). In order to determine how many tiles are inside the ImageAtlas, I need to know the tiles per row, and the tiles per column, and of course, the image. This information is know by the person that has created the ImageAtlas, the code cannot determine it.
This way is very easy replacing the imageAtlas with another one with more tiles without changing the code.

{
	class:class:entity.Entity,
	
	list:componentList: {
		class:class:components.Transformation,
		Float:xTopLeft:0.5,
		Float:yTopLeft:0.0,
		Float:xWidth:0.5,
		Float:yHeight:0.5,
	},
		
	list:componentList: {
		class:class:components.ImageAtlas,
		int:tilesPerRow:4,
		int:tilesPerCol:4,
		int:index:1,
		Image:image:hangman_atlas.png,
	},	
}

Of course, this design flexibility is not for free. Your code could seem a bit more complex.
And refactoring the properties or classes names will not automatically update the configuration files…

On the other hand, a very good benefit of extracting the data is that strings will be extracted so you will be able to easily internationalize your code.

object:ls:{
	class:class:hangman.LocalizedStrings,
	char:hiddenChar:_,
	String:header:  HANGMAN  ,
	String:win:VICTORIA!,
	String:lose:Has perdido. Era ,
	String:no:Ninguna,
	String:tryAgain:prueba de nuevo,
	String:left:quedan.,
},

object:ls:{
	class:class:hangman.LocalizedStrings,
	char:hiddenChar:_,
	String:header:  HANGMAN  ,
	String:win:YOU WIN!,
	String:lose:You lose. It was ,
	String:no:No,
	String:tryAgain:try again,
	String:left:left.,
},

Finally, let me share the new main class with all the data extracted

package manager;

import components.ImageAtlas;
import components.Text;
import entity.Entity;
import entity.EntityBuilder;
import factory.IIOFactory;
import factory.IOFactory;
import factory.IOType;
import gateways.IKeyboardInput;
import gateways.IScreenOutput;
import hangman.LogicHangman;
import permanentStorage.IPermanentStorage;
import permanentStorage.PermanentStorageType;

public class Hangman {

	public static void main(String[] args) {

		// Builder
		IIOFactory factory = new IOFactory();
		// Choose input
		IKeyboardInput in = factory.buildInput(IOType.AWT);
		// Choose output
		IScreenOutput out = factory.buildOutput(IOType.AWT);
		// Choose storage
		IPermanentStorage storage = factory.makePermanentStorage(PermanentStorageType.RESOURCE);
		
		// Load entities
		Entity lifes = EntityBuilder.getEntityFromMetaFile(storage, "lifes.meta");
		Entity textBox = EntityBuilder.getEntityFromMetaFile(storage, "textbox.meta");
		Entity hangmanLogic = EntityBuilder.getEntityFromMetaFile(storage, "logicHangman.meta");
		LogicHangman hangman = hangmanLogic.getComponent(LogicHangman.class);
		
		// Game loop
		while (true) {

			// Initial Scene
			hangman.generateNewWord();
			hangman.startAnimation("game");
			lifes.getComponent(ImageAtlas.class).setIndex(hangman.getAnimationID());	
			textBox.getComponent(Text.class).clear();
			textBox.getComponent(Text.class).appendLine(hangman.getHeaderString());
			textBox.getComponent(Text.class).appendLine(hangman.getCurrentRevealedWord());
	
			out.clear();
			out.draw(textBox);
			out.draw(lifes);
			
			// 2. Match loop
			while (hangman.getAttempts() > 0 && !hangman.isWordCompletelyGuessed()) {
				
				// 2.1 User input
				char mchar = in.readChar();
				
				// 2.2 Update
				if ( hangman.guess(mchar) == true ) {
					textBox.getComponent(Text.class).appendLine(hangman.getCurrentRevealedWord());
					if ( hangman.isWordCompletelyGuessed() ) {
						hangman.startAnimation("win");
						lifes.getComponent(ImageAtlas.class).setIndex(hangman.getAnimationID());
						textBox.getComponent(Text.class).appendLine(hangman.getWinString());
					}
				} else {
					hangman.decreaseAttempts();
					textBox.getComponent(Text.class).appendLine(hangman.getTryAgain(mchar));
					hangman.advanceAnimation();
					lifes.getComponent(ImageAtlas.class).setIndex(hangman.getAnimationID());
					if ( hangman.getAttempts() <= 0 ) {
						textBox.getComponent(Text.class).appendLine(hangman.getLoseString() + " " + hangman.getSecretWord());
					}
				}
				
				// 2.3 Draw
				out.clear();
				out.draw(textBox);
				out.draw(lifes);
			}
			
			// 3. Game close
			if ( in.readChar() == '0' ) { 
				System.exit(0);
			}
			
		}
	}
}

Using the configuration files I have modified the behavior of the game to let the player only 4 tries, and of course, the tiles needed for the animation have been also redefined.

	list:componentList: {
		class:class:hangman.LogicHangman,
		int:totalAttempts:4,
		object:animation:{
			class:class:hangman.Animation,
			IntegerArray:animation1:0;2;3;5;7,
			IntegerArray:animation2:8;9,
		},		


Please, share your thoughts in the comments or send me a message.

Thanks for reading.

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement