Monday 29 June 2015

Android and Java

My main track of learning has been in Java, and as such I have of course found an interest in learning to work with the Android platform. My own smartphone is an Android, we own a tablet which is Android and I see myself continuing to make purchases of this type in the future. I got a couple of Android game programming books for Christmas and one of them is too simple (copy what we wrote and don't ask questions), the other assumes a lot more knowledge and has taken me a while to catch up to.

Before learning Java more comprehensibly through the Head First Java book, I studied via a web series from developer John Purcell. The course is available on Udemy and I highly recommend it for anyone looking to get a grip on Java while seeing someone type and run code into the Eclipse IDE.

There is also an Android course available by John Purcell which I hope to take in once I have a handle of the basics. My experience so far has been that studying from one source and then getting that second look from another really helps to solidify my understanding. Seeing one example of a new concept compared to another is often the deciding factor on whether I "get it" or need to go to bed because I am too tired.

My current understanding of Android is of the Manifest and how to add activities, the use of a few basic View's, some simple touch handling and basic drawing to a Canvas. I am working on an activity that takes in multiple touches (pointers) and assigns them to a new or incomplete pair. For each pointer, if it belongs to a pair but has no other pointer, it draws a circle, and for complete pairs (both pointers still touching the screen) it draws smaller circles and a line to connect them. My code for this has gone through a few iterations as I figure out the pointers for each MotionEvent that is fired and handle it, but the drawing aspect has me stuck right now. My program may just be over complicated (using a class instance for each PointerPair) and I may have to go back to simple arrays to keep track of each pointer and its relationship. for now though my code looks like this.

There are a few Log.d debugging lines in there for when I test on my phone. It took a while for me to realize that my code was not functioning and cloned my initial PointerPair instance to all slots of the array because I didn't exit the for loop when I should have. I understand my code isn't pretty and probably makes some ridiculous optimization choices but its main objective is to help me learn so I'm not too precious yet :P

(I used hilite.me for code formatting)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package ***********;

import java.util.ArrayList;
import java.util.Random;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.Window;
import android.view.WindowManager;

public class DrawLinesTest extends Activity implements OnTouchListener{
 private final int MAX_PAIRS = 5;
 PointerPair[] pairs = new PointerPair[MAX_PAIRS];
 Random rng;
 PointerPair openPair = null;
 
 ArrayList<PointerPair> drawables = new ArrayList<PointerPair>();
 
 
 public void onCreate(Bundle savedInstanceState){
  super.onCreate(savedInstanceState);
  requestWindowFeature(Window.FEATURE_NO_TITLE);
  getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
  RenderView v = new RenderView(this);
  v.setOnTouchListener(this);
  setContentView(v);
  
  rng = new Random();
 }
 
 public void onResume(){
  super.onResume();
  for(int i = 0; i < pairs.length; i++){
   pairs[i] = null;
  }
 }
 
 @TargetApi(Build.VERSION_CODES.FROYO)
 public boolean onTouch(View v, MotionEvent e){
  //Log.d("PointerPair", "Screen Touched");
  int action = e.getActionMasked();
  int pointerIndex = e.getActionIndex();
  int pointerId = e.getPointerId(pointerIndex);
  
  switch(action){
  case MotionEvent.ACTION_DOWN:
  case MotionEvent.ACTION_POINTER_DOWN:
   if(openPair != null){
    openPair.setPointer2Id(pointerId);
    openPair.setCoords(1, e.getX(), e.getY());
    Log.d("PointerPair", "Pairs: An pointer partner has been found! ");
    openPair = null;
   } else {
    boolean pairCreated = false;
    for(int i = 0; i < pairs.length; i++){
     if(!pairCreated){
      if(pairs[i] == null){
       Log.d("PointerPair", "Pairs: Creating new pair at index " + i);
       pairs[i] = new PointerPair(pointerId, e.getX(), e.getY());
       drawables.add(pairs[i]);
       Log.d("PointerPair", "Pairs: An pointer needs a partner! ");
       openPair = pairs[i];
       pairCreated = true;
      }
     }
    }
   }
   break;
  case MotionEvent.ACTION_MOVE:
   for(int i = 0; i < pairs.length; i++){
    if(pairs[i] != null){
     if(pointerId == pairs[i].pointer1Id){
      pairs[i].setCoords(0, e.getX(), e.getY());
     } else if(pointerId == pairs[i].pointer2Id){
      Log.d("PointerPair", "Painting - updating second pointer coords");
      pairs[i].setCoords(1, e.getX(), e.getY());
     }
    } 
   }
   break;
  case MotionEvent.ACTION_POINTER_UP:
   for(int i = 0; i < pairs.length; i++){
    if(pairs[i] != null){
     if(pointerId == pairs[i].pointer1Id){
      //Log.d("PointerPair", "Pairs: Lifted Pointer matches pointer 1 in pair ID:" + i);
      pairs[i].pointer1Id = -1;
      
     } else if(pointerId == pairs[i].pointer2Id){
      //Log.d("PointerPair", "Pairs: Lifted Pointer matches pointer 2 in pair ID:" + i);
      pairs[i].pointer2Id = -1;
     }
    } 
   }
   break;
  case MotionEvent.ACTION_UP:
   for(int i = 0; i < pairs.length; i++){
    pairs[i] = null;
    drawables.clear();
   }
   openPair = null;
   break;
  }
  
  cleanPairs();
  v.invalidate();
  return true;
 }
 
 private void cleanPairs(){
  for(int j = 0; j < pairs.length; j++){
   if(pairs[j] != null){
    pairs[j].update();
   }
  }
  
  // Clean out dead pairs
  for(int i= 0; i < pairs.length; i++){
   if(pairs[i] != null){
    if(pairs[i].pairDead()){
     drawables.remove(pairs[i]);
     //Log.d("PointerPair", "Pairs: Nulling a pair at position" + i);
     pairs[i] = null;
    }
   }
  }
 }
 
 class PointerPair{
  private int pointer1Id, pointer2Id;
  private boolean needsPair, isPointer1Dead, isPointer2Dead, isPairDead;
  private int p1x, p1y, p2x, p2y;
  Paint paint;
  
  
  public PointerPair(int pointer1Id){
   this.pointer1Id = pointer1Id;
   pointer2Id = -1;
   needsPair = true;
   paint = new Paint();
   paint.setARGB(255, rng.nextInt(255), rng.nextInt(255), rng.nextInt(255));
  }
  
  public PointerPair(int pointer1Id, float x, float y){
   this(pointer1Id);
   setCoords(0, x, y);
  }
  
  public void setCoords(int pointerIndex, float x, float y){
   if(pointerIndex == 0){
    p1x = (int) x;
    p1y = (int) y;
   } else if(pointerIndex == 1){
    p2x = (int) x;
    p2y = (int) y;
   }
  }
  
  public void update(){
   if(pointer1Id == -1){
    isPointer1Dead = true;
   }
   if (pointer2Id == -1){
    isPointer2Dead = true;
   }
   
   if(isPointer1Dead && isPointer2Dead){
    isPairDead = true;
   } else if (isPointer1Dead && needsPair){
    isPairDead = true;
   }
  }
  
  public boolean needsPair(){
   return needsPair;
  }
  
  public void setPointer2Id(int pointer2Id){
   this.pointer2Id = pointer2Id;
   needsPair = false;
  }
  
  public boolean pairDead(){
   return isPairDead;
  }
  
  public Paint getPaint(){
   return paint;
  }
 }
 
 class RenderView extends View{
  Paint paint;
  
  public void onDraw(Canvas canvas){
   
   clear(canvas);
   for(int i = 0; i < pairs.length; i++){
    for(PointerPair p: drawables){
     Paint paint1 = p.getPaint();
     
     if(p.pointer1Id != -1 && p.pointer2Id != -1){
      Log.d("PointerPairPaint", "Painting a pair, 2 circles baby!s");
      canvas.drawCircle(p.p1x, p.p1y, getWidth() / 15, paint1);
      canvas.drawCircle(p.p2x, p.p2y, getWidth() / 15, paint1);
      return;
     } 
     
     if(p.needsPair() || p.isPointer2Dead){
      Log.d("PointerPairPaint", "Painting lonely first pointer, " + p.p1x + ", " + p.p1y);
      canvas.drawCircle(p.p1x, p.p1y, getWidth() / 10, paint1);
      return;
     } else if(p.isPointer1Dead){
      Log.d("PointerPairPaint", "Painting lonely second pointer, " + p.p2x + ", " + p.p2y);
      canvas.drawCircle(p.p2x, p.p2y, getWidth() / 10, paint1);
      return;
     }
    }
   }
   //invalidate();
  }
  
  public RenderView(Context context){
   super(context);
   paint = new Paint();
  }
  
  private void clear(Canvas c){
   paint.setColor(Color.GRAY);
   c.drawRect(0, 0, getWidth() - 1, getHeight() - 1, paint);
  }
  
 }
 

}

Sunday 28 June 2015

3 Way Battle Simulation

So in my previous post I made reference to a simulation of battles between Mages, Rangers and Warriors. Just thought I'd post it here for anyone to look through and play with! The Barracks.java class is the runnable, and will output a .rep file (simple text document, open with Notepad) with the breakdown of the battle. It wouldn't be too hard to add more info to the report. you can tweak the attack power and defense or any of the other stats for each of the classes.

Mob Class (Parent for the three rpg-classes)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package saving;

import java.io.Serializable;

@SuppressWarnings("serial")
public class Mob implements Serializable{
 
 int maxHealth;
 int health;
 String name;
 int attack;
 double defence;
 double accuracy;
 double dodge;
 String type;
 private String battleReport = null;
 private String attackReport = null;
 
 private boolean isAlive;
 
 public Mob(){
  isAlive = true;
 }
 
 public void takeDamage(int damage){
  int absorb = (int) (damage * defence);
  if(damage - absorb >= 1){   
   health -= (damage - absorb);
  }
  
  battleReport = name +  " absorbs " + absorb + " damage.\n" + name + " takes " + (damage - absorb) + " damage\nHealth remaining: " + health + "\n";
  
  if(health <= 0) {
   health = 0;
   isAlive = false;
  }
 }
 
 public void attack(Mob enemy){
  attackReport = null;
  battleReport = null;
  enemy.attackReport = null;
  enemy.battleReport = null;
  attackReport = (name + " is attacking " + enemy.name);
  if(Math.random() > accuracy){
   // Missed
   attackReport += "\nAttack missed. End of attack.";
   enemy.battleReport = enemy.name + " is unharmed\n";
   return;
  }
  
  if(Math.random() < enemy.dodge){
   // Dodged
   attackReport += "\n" + enemy.name + " dodged the attack. End of attack.\n";
   enemy.battleReport = enemy.name + " is unharmed\n";
   return;
  }
  
  int attackDamage = attack;
  if ((type.equals("Orc")  && enemy.type.equals("Elf")) 
    || (type.equals("Human") && enemy.type.equals("Orc"))
    || (type.equals("Elf") &&  enemy.type.equals("Human"))){
   attackDamage *= 1.2;
   attackDamage -= Math.random() * attackDamage;
  } else if((type.equals("Orc") && enemy.type.equals("Human")) 
    || (type.equals("Human") && enemy.type.equals("Elf"))
    || (type.equals("Elf") && enemy.type.equals("Orc"))){
   attackDamage *= 0.8;
   attackDamage -= Math.random() * attackDamage + 1;
  }
  enemy.takeDamage(attackDamage);
 }
 
 public void heal(int hpUp){
  health += hpUp;
  if(health > maxHealth) health = maxHealth;
 }
 
 public String toString(){
  return "MOB: " + this.hashCode() + "\nHealth: " + health +"\nAlive: " + isAlive; 
 }
 
 public boolean isAlive(){
  return isAlive;
 }
 
 public String getBattleReport(){
  return battleReport;
 }
 
 public String getAttackReport(){
  return attackReport;
 }

}

Main Class - Barracks.java

Edit the COMBATANTS constant to change how many mobs are in each army, note, it will take a lot longer to run if you go too high!

1000 combatants (3,000 total) took approx 0.5 seconds
10,000 combatants (30,000 total) took approx 5 seconds
100,000 combatants (300,000 total) took approx 62 seconds
1,000,000 Could not run, Out of Memory.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package saving;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

public class Barracks {

 private final int COMBATANTS = 500; // exceed 100,000 at your own risk.
 private int skirmishes;
 
 @SuppressWarnings("rawtypes")
 private ArrayList<ArrayList> mobs;
 
 public static void main(String[] args) {
  Barracks b = new Barracks();
  b.go();
 }
 
 public void go(){
  long startTime = System.currentTimeMillis();
  System.out.println("Starting simulation: Number of Combatants per army = " + COMBATANTS);
  // Armies of Mobs
  ArrayList<Orc> orcs = new ArrayList<Orc>();
  ArrayList<Elf> elves = new ArrayList<Elf>();
  ArrayList<Human> humans = new ArrayList<Human>();
  
  // Populate armies
  for(int i = 0; i < COMBATANTS; i++){
   orcs.add(new Orc("Warrior" + i));
   elves.add(new Elf("Ranger" + i));
   humans.add(new Human("Mage" + i));
  }

  // Armies added to array list
  mobs = new ArrayList<>();
  mobs.add(orcs);
  mobs.add(elves);
  mobs.add(humans);
  
  try {
   // Create text report
   FileWriter fw = new FileWriter("BattleReport.rep");
   BufferedWriter bw = new BufferedWriter(fw);
   // Report title
   bw.write("Battle between " + COMBATANTS + " orcs, elves and Humans:");
   
   // Main simulation loop
   while(!orcs.isEmpty() && !elves.isEmpty() && !humans.isEmpty()){
    // While all armies have combatants, select to opposite faction mobs
    int class1 = (int) (Math.random() * 3);
    int mobNum = (int) (Math.random() * mobs.get(class1).size());
    Mob fighter1 = (Mob) mobs.get(class1).get(mobNum);
    
    // Prevent same faction skirmish
    int class2 = (int) (Math.random() * 3);
    while(class2 == class1){
     class2 = (int) (Math.random() * 3);
    }
    int mob2Num = (int)(Math.random() * mobs.get(class2).size());    
    Mob fighter2 = (Mob) mobs.get(class2).get(mob2Num);
    
    // Actual skirmish
    fighter1.attack(fighter2);
    // Get report on actions taken
    bw.write("\n" + fighter1.getAttackReport());
    // Get report on hits taken and damage mitigation
    bw.write("\n" + fighter2.getBattleReport());
    // If damage is taken, increment skirmishes counter
    if(fighter2.getBattleReport().contains("takes")){
     skirmishes++;
    }
    if(!((Mob) mobs.get(class1).get(mobNum)).isAlive()){
     mobs.get(class1).remove(mobNum);
    } else if(!((Mob) mobs.get(class2).get(mob2Num)).isAlive()){
     mobs.get(class2).remove(mob2Num);
    }
    
   }
   long delta = System.currentTimeMillis() - startTime;
   double elapsedTime = delta / 1000.0;
   System.out.println("Ending simulation: Number of Skirmishes = " + skirmishes + "\nTime taken: " + elapsedTime + "sec");
   
   // Write out summary of battles
   bw.write(battleReport());
   bw.close();
   
  }catch (IOException e){
   
  }
  
 }
 
 public String battleReport(){
  String report = null;
  // Find defeated army and display remaining enemies and total skirmishes
  if(mobs.get(0).isEmpty()){
   report = "\nOrcs are defeated" +
   "\n" + mobs.get(1).size() + " Elves remain" +
   "\n" + mobs.get(2).size() + " Humans remain";
  } else if (mobs.get(1).isEmpty()){
   report = "\nElves are defeated" +
     "\n" + mobs.get(0).size() + " Orcs remain" +
     "\n" + mobs.get(2).size() + " Humans remain";
  }else if (mobs.get(2).isEmpty()){
   report = "\nHumans are defeated" +
     "\n" + mobs.get(0).size() + " Orcs remain" +
     "\n" + mobs.get(1).size() + " Elves remain";
  }

  return (report + "\n" + skirmishes + " total Skirmishes");
 }
}

Elf.java - The Ranger class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package saving;

@SuppressWarnings("serial")
public class Elf extends Mob{
 
 public Elf(String name){
  super();
  this.name = name;
  maxHealth = 180;
  health = maxHealth;
  type = "Elf";
  
  attack = 16;
  defence = 0.15;
  accuracy = 0.95;
  dodge = 0.06;
 }
}

Orc.java - The Warrior class


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package saving;

@SuppressWarnings("serial")
public class Orc extends Mob{
 
 public Orc(String name){
  super();
  this.name = name;
  maxHealth = 150;
  health = maxHealth;
  type = "Orc";
  
  attack = 20;
  defence = 0.30;
  accuracy = 0.85;
  dodge = 0.1;
 }
}

Mage.java - The Mage class


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package saving;

@SuppressWarnings("serial")
public class Human extends Mob{

 public Human(String name){
  super();
  this.name = name;
  maxHealth = 140;
  health = maxHealth;
  type = "Human";
  
  attack = 30;
  defence = 0.12;
  accuracy = 0.90;
  dodge = 0.05;
 }
 
}

A bit of background

Self learning ain't easy

Being a self taught programmer and game designer is definitely not going to be easy, I am already finding the task of finding good material to learn from to be a problem. Often when I am finished with one resource, the next either assumes a greater level of knowledge or falls back to basics.

My main resources for learning Java have so far been:
  • Codecademy: My first look at coding was the Javascript track which I began playing with just from curiosity. While this is not the best track to learn programming, at the time this was the only one available. Ruby and Python came later with redesigned tracks to support the programming route as a whole.
  • CS106A Stanford Programming Methodology: Lectures by Professor Mehran Sahami
    This series of lectures is available on Youtube and has assignment material available via the Stanford website. This course first introduced me to Java with Karel, the little robot.
  • Java - An introduction to Problem Solving and Programming: Walter Savitch (5th Edition)
    This textbook was donated to me by a work colleague who had taken the course as part of her university education but had no interest in pursuing it further. The book helped me to move away from the safe environment of Stanford's custom Java work space and onto making my own applications within Eclipse. I am currently using Eclipse Luna version 4.4.1
  • TheCherno: Youtube Game Programming series
    This series was a love/hate one, I enjoyed the coding and following along with a project and being able to tinker was very fun, but I think while the aim was for newer programmers to use this series as an introduction to game programming, it sometimes left me wondering what the heck I was doing. I stopped following this series after about 60 episodes but it definitely contributed to inspiring my continuation of studies.
  • After a period of stagnation and loss of direction, I finally found Head First Java, which took a couple of attempts for me to work with. At first I found the quirky sense of humor to be annoying and tired of it. It took another couple of months before I gave it another try (following some reddit advice) and got through my brick wall.

Recently my studies in Java and Android have also been accompanied by a read through of Fundamentals of Game Design by Ernest Adams. Reading this has helped me to pin down the vocabulary I need for my designs and also given me new ways to examine the games I draw inspiration from. So far I'm into the third chapter. I plan to give the book a good read through and then return to it when I put my ideas down to make sure I am doing what I need in an industry standard way.

The game I am working on designing and coding is an economic simulation in a food service setting. My ideas have been littered around my apartment in different forms without any direction, but the book is helping me to organize my approach. It's still important to write down or doodle my ideas, but I am now trying to keep the wild "this feature would be amazing!" type ideas from taking up too much time. Once I have a working prototype and my core mechanics working properly, then the features will be tested and discarded or developed as necessary.

I have also been looking into keeping an offline private wiki as a means to keep all ideas and documents I write accessible from one place. For this I have started learning to use a program called Wikidpad. With such little organization so far, there's not much to put into a wiki but as I bring my design ideas together and throw away the garbage, I intend to post snippets of wiki pages for people to read here too.

Simulations

My main interest is in simulations, and as I have gained new skills I have done my best to create small scenarios in code that behave in ways typical of simulations. For instance, once I learned about ArrayList and the QueueBlocking Arrays, I put together a background service in the mini restaurant testing program I have to create parties of guests at random intervals, assigning them to different Queues based on the size of the party, and limiting the combined total number of queueing parties. When a "table" became available, the service (nicknamed the Hostess) would assign an appropriate sized party to the empty table and free up a space in the queue.

More recently, while learning about serialization techniques, I created a mini simulation based on the classic fantasy triangle of damage. Warrior > Ranger > Magic User > Warrior. This is a common strengths/weakness model, most notably for me found in Runescape. The simulation pitted armies of each type into a war where each of the three types is randomly paired into a skirmish with a member of one of the other armies. Their fight gave an outcome and eventually all members of an army died and the results were saved out to a text file.

An example of the output is shown to the right, of a battle between 50 "mobs" per army. It was very fun to tweak some of the stats I had given each class to see how the outcome was affected. Getting the numbers to become pretty fair was surprisingly sensitive, as even a slight increase or decrease to a stat sometimes swung the outcome a completely different direction.

This type of simulation taught me a lot about how to work with arrays and random number generation. Having to check the arrays for "dead" mobs and delete them, creating damage and damage absorption that is calculated on the fly in each skirmish and even adding in a dodge and accuracy stat really mixed up the outcomes.

Starting simulation: Number of Combatants per army = 500
Ending simulation: Number of Skirmishes = 2806
Time taken: 0.279sec


Moving forward

My intention with this blog is to create a place for new programmers to look to see my trials and tribulations. If anyone can gather any knowledge, resources or inspiration from me then this blog will have served a purpose. For my own needs, I want to use this as a place to summarize my studies, hold myself accountable, and also to see where I was at a particular stage of my journey. Please feel free to get in touch if you have any questions or comments. Thanks for reading!