- Separating Abstraction from Implementation
- Dice, and Visual Representations of Dice
- Playing Sound Files with API Calls
- Managing Object State Change with the Observer Pattern and Events
Dice, and Visual Representations of Dice
In the context of our game of Dihtzee, a reasonable abstraction is that of a die. A further decomposition is to divide the die into pure representation of a “something” that can contain one of six possible random values and a visual representation of that something.
What about games that use something besides six-sided dice (although Dungeons and Dragons is the only such game I can think of at the moment)? Could we create a representation of a die with n-sides, generalize that to create a six-sided die and a graphical six-sided die? What about something die-like that is n-sided, but uses something other than integers to represent the face value of the die? Both abstractions are obviously possible. But this is where we have to figure out how much decomposition is worth doing; otherwise we could be sub-dividing classes ad infinitum.
For many dice games—in fact, most such games that I can think of—a six-sided die is sufficient. Consequently, while cognizant of n-sided, semantically valued dice, a six-sided die and its graphical representation are both reasonable and sufficient. This is what is meant by deliberate abstraction.
Defining the Die Class
For our purpose, we can define a Die class that can have one value between 1 and 6 inclusive at any point in time. We can also state that a Die can be rolled as a means of determining the value of the Die. Further, we might want to know if the Die is currently in between states or if it's rolling, and when the Die finishes rolling. Because these states and behaviors are metaphors for the physical world, we also need to define analogous representations.
The Die value can be represented by an enumeration of the possible face values of the Die. We can represent the Roll behavior with the random number generation behavior of the Random class. The IsRolling state can be represented by a Boolean, and, to indicate when the Die has rolled to a new value-position, we might implement an event called OnFaceChanged. Listing 1 contains the Die class and a small supporting cast.
Listing 1: The DieFace enumeration, Die class, and a new DieEventArgs class.
using System; namespace RollOfTheDice { public class DieEventArgs : EventArgs { private DieFace face = DieFace.None; public DieEventArgs() : base(){} public DieEventArgs(DieFace face) : base() { this.face = face; } public DieFace Face { get{ return face; } } } public enum DieFace { None=0, One=1, Two=2, Three=3, Four=4, Five=5, Six=6 } public delegate void ChangedHandler(object sender, DieEventArgs e); public class Die { private DieFace currentFace = DieFace.None; private bool isRolling = false; public ChangedHandler OnChanged; public Die(){} public Die(DieFace face) { currentFace = face; } public bool IsRolling { get{ return isRolling; } } public DieFace CurrentFace { get{ return currentFace; } set { currentFace = value; DoChanged(value); } } public void Roll() { if(isRolling) return; isRolling = true; try { DoRoll(); } finally { isRolling = false; } } protected virtual void DoRoll() { Random roller = new Random(DateTime.Now.TimeOfDay.Milliseconds); Random picker = new Random(DateTime.Now.TimeOfDay.Milliseconds); for(int i=0; i<roller.Next(10, 20); i++) { currentFace = (DieFace)picker.Next((int)DieFace.One, (int DieFace.Six); DoChanged(currentFace); } } private void DoChanged(DieFace face) { if( OnChanged != null ) OnChanged(this, new DieEventArgs(face)); } } }
Let's make sure this works. We can employ the enumeration and two classes to contrive a simple guessing game. Listing 2 contains the GuessTheRole, demonstrating that our Die abstraction is indeed functional at a fundamental level.
Listing 2: GuessTheRole demonstrates the basic functionality of our Die class.
using System; using RollOfTheDice; namespace GuessTheRole { class Class1 { private static Die die = new Die(); [STAThread] static void Main(string[] args) { die.OnChanged += new ChangedHandler(OnChanged); while(true) { Console.WriteLine("Guess the roll (1-6, Q=Quit)"); string guess = Console.ReadLine(); if( guess == "q" || guess == "Q" ) return; int guessAsInt = -1; try { guessAsInt = Convert.ToInt32(guess); if( guessAsInt < 1 || guessAsInt> 6) continue; } catch { Console.WriteLine("Invalid guess"); continue; } die.Roll(); while(die.IsRolling); if( (int)die.CurrentFace == guessAsInt) Console.WriteLine("You guesed correctly!"); else Console.WriteLine("You guessed incorrectly!"); } } private static void OnChanged(object sender, DieEventArgs e) { Console.WriteLine("Rolling: " + e.Face.ToString()); } } }
As you might imagine, GuessTheRoll elicits a number, rolls the die and indicates whether you guessed correctly or not. Unless you are a brand new programmer, it isn't very interesting. However, what it does do is demonstrate that at this level of abstraction our Die-implementation is reasonable, sufficient, and complete.
Defining a Visual Die
Years ago, I enjoyed playing puzzle or mystery games in which the player typed in simple text commands and responded to verbal cues. However, since the very early days of graphics adaptors, these games became significantly less compelling than graphics-based games. As a result, we really need a visual representation of our Die class.
Again, we have to decide what is reasonable and sufficient. Will a two-dimensionally rolling Die be enough, or does the Die need to appear to move in three dimensions? A three-dimensional Die may be more interesting, but for our purposes (and based on the time available before this article is due—we all have deadlines) a two-dimensional Die will have to suffice (see listing 3).
Listing 3: The VisualDie and supporting cast.
1: using System; 2: using System.Diagnostics; 3: using System.Drawing; 4: using System.Threading; 5: using System.Windows.Forms; 6: 7: namespace RollOfTheDice 8: { 9: public interface IDiePainter 10: { 11: void Draw(object canvas, Die die, Rectangle bounds); 12: } 13: 14: public class GDIDiePainter : IDiePainter 15: { 16: #region IDiePainter Members 17: 18: public void Draw(object canvas, Die die, Rectangle bounds) 19: { 20: Debug.Assert(canvas is Graphics); 21: DrawDie(canvas as Graphics, die, bounds); 22: } 23: 24: #endregion 25: 26: private void DrawDie(Graphics g, Die die, Rectangle bounds) 27: { 28: // draw basic shape 29: Rectangle r = bounds; 30: g.DrawRectangle(Pens.Black, r); 31: r.Inflate(-1, -1); 32: g.FillRectangle(Brushes.Ivory, r); 33: 34: // fill in the face 35: if( die.CurrentFace != DieFace.None) 36: DrawDots(g, GetRects((int)die.CurrentFace, r)); 37: } 38: 39: private void DrawDots(Graphics g, Rectangle[] rects) 40: { 41: for( int i=0; i < rects.Length; i++) 42: DrawDot(g, rects[i]); 43: } 44: 45: private Rectangle[] GetRects(int value, Rectangle bounds) 46: { 47: Rectangle[] one = new Rectangle[]{GetRectangle(1,1, bounds)}; 48: Rectangle[] two = new Rectangle[]{GetRectangle(0, 2, bounds), 49: GetRectangle(2, 0, bounds)}; 50: Rectangle[] three = new Rectangle[]{GetRectangle(0, 2, bounds), 51: GetRectangle(1, 1, bounds), GetRectangle(2, 0, bounds)}; 52: Rectangle[] four = new Rectangle[]{GetRectangle(0, 0, bounds), 53: GetRectangle(0, 2, bounds), GetRectangle(2, 0, bounds), 54: GetRectangle(2, 2, bounds)}; 55: Rectangle[] five = new Rectangle[]{GetRectangle(0, 0, bounds), 56: GetRectangle(1, 1, bounds), GetRectangle(0, 2, bounds), 57: GetRectangle(2, 0, bounds), GetRectangle(2, 2, bounds)}; 58: Rectangle[] six = new Rectangle[]{GetRectangle(0, 0, bounds), 59: GetRectangle(0, 1, bounds), GetRectangle(0, 2, bounds), 60: GetRectangle(2, 0, bounds), GetRectangle(2, 1, bounds), 61: GetRectangle(2, 2, bounds)}; 62: 63: Rectangle[][] rects = {one, two, three, four, five, six}; 64: return rects[value-1]; 65: } 66: 67: private void DrawDot(Graphics g, Rectangle r) 68: { 69: g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; 70: r.Inflate(-3, -3); 71: g.FillEllipse(new SolidBrush(Color.Black), r); 72: } 73: 74: private Rectangle GetRectangle(int x, int y, Rectangle bounds) 75: { 76: return new Rectangle(bounds.X + (bounds.Width * x / 3), 77: bounds.Y + (bounds.Height * y / 3), 78: GetDotSize(bounds).Width, GetDotSize(bounds).Height); 79: } 80: 81: private Size GetDotSize(Rectangle bounds) 82: { 83: return new Size(bounds.Width / 3, bounds.Height / 3); 84: } 85: } 86: 87: 88: public class VisualDie : Control 89: { 90: private Die internalDie = new Die(DieFace.None); 91: private IDiePainter painter = new GDIDiePainter(); 92: public VisualDie() 93: { 94: internalDie.OnChanged += new ChangedHandler(OnChanged); 95: } 96: 97: public VisualDie(DieFace face) : base() 98: { 99: internalDie.CurrentFace = face; 100: internalDie.OnChanged += new ChangedHandler(OnChanged); 101: } 102: 103: public void Roll() 104: { 105: internalDie.Roll(); 106: } 107: 108: public bool IsRolling 109: { 110: get{ return internalDie.IsRolling; } 111: } 112: 113: public DieFace CurrentFace 114: { 115: get{ return internalDie.CurrentFace; } 116: set{ internalDie.CurrentFace = value; } 117: } 118: 119: protected override void OnPaint(PaintEventArgs pe) 120: { 121: painter.Draw(pe.Graphics, internalDie, ClientRectangle); 122: } 123: 124: private void OnChanged(object sender, DieEventArgs e) 125: { 126: 127: this.Refresh(); 128: Application.DoEvents(); 129: Thread.Sleep(25); 130: } 131: } 132: }
The VisualDie is a bit more complex. We use aggregation, inheritance, GDI+, events, and implementing an interface. For this reason, I added line numbers for reference. (Remember to leave the line numbers out of your code.) However, the amount of code is substantially less, because a chunk of the work is being done in our original Die class.
Lines 9 through 12 define the IDiePainter interface. Lines 14 through 85 define an implementation of the IDiePainter class, GDIDiePainter, and lines 88 through 131 implement the VisualDie class. The result of this code can be seen in figure 1.
Figure 1: A Die with a face value of 5.
IDiePainter defines an interface that requires a GDI+ Graphics object, an instance of our Die class, and a bound rectangular region. The latter indicates how big the visual die should be. IGDIDiePainter implements IDiePainter for GDI+. (We could easily “paint” the Die in some other manner by creating an additional class that implements IDiePainter.) Finally, the VisualDie is responsible for orchestrating all of this code into the Die you see in figure 1.
IGDIDiePainter handles the visual and animated portion of the VisualDie. The VisualDie has a graphics object; it will receive paint messages because it inherits from System.Windows.Forms.Control.
Because VisualDie inherits from Control and multiple inheritance isn't supported, we use aggregation by making an instance of the Die class a member of VisuaDie. To make the VisualDie look and feel like a Die, we surface the constituent properties CurrentFace and IsRolling and methods Roll
of the contained Die object. Because VisualDie does not inherit from Die we cannot use it in place of Die in a polymorphic sense. However, because it helped our implementation this is okay; inheriting from Control makes it easier to update and paint the VisualDie. (Controls receive Paint messages from Windows.)