I tried this as a programming exercise. It's working as intended, but I feel like the code is a mess and I could have used the advantages of object oriented programming much more (like inheritance for fish and shark from the Cell
class, and probably much more). Can someone guide me a bit through this code example and show me how to make this more object-oriented?
The code simulates an ocean populated by fish and sharks:
- Sharks hunt fish
- Fish swim and breed
- Sharks die of starvation after x years
The ocean, fish and sharks are all represented by a Cell
class. The objects are stored in a 2D array and then painted onto the Windows form.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
using System.Threading;
namespace WaTor
{
public partial class Form1 : Form
{
//Gritsize width(x) and height(y)
const int TableX = 10;
const int TableY = 10;
const int pixelSizeX = 19;
const int pixelSizeY = 19;
//Fish Properties
int initialNumberOfFish = 2;
int initialNumberOfSharks = 2;
int ageToBreedFish = 5;
int ageToBreedShark = 10;
int yearToStarvation = 5;
int sharkScent = 3;
Random rnd = new Random();
Cell[,] myArray = new Cell[TableX, TableY];
public Form1()
{
InitializeComponent();
intitializeArray();
}
private void intitializeArray()
{
int x = 0; //Position on X-Axis
int y = 0; //Position on Y-Axis
for (int i = 0; i < myArray.GetLength(0); i++)
{
for (int j = 0; j < myArray.GetLength(1); j++)
{
Cell newCellX = new Cell();
myArray[i,j] = newCellX;
myArray[i,j].posX = x;
myArray[i,j].posY = y;
x = x + pixelSizeX+1;
}
x = 0;
y = y + pixelSizeY+1;
}
}
//Only used to paint the results
private void Canvas_Paint(object sender, PaintEventArgs e)
{
Graphics g = Canvas.CreateGraphics();
for (int i = 0; i < myArray.GetLength(0); i++)
{
for (int j = 0; j < myArray.GetLength(1); j++)
{
Brush b = new SolidBrush(myArray[i,j].cellColor);
Rectangle r = new Rectangle(myArray[i,j].posX, myArray[i,j].posY, pixelSizeX, pixelSizeY);
g.FillRectangle(b, r);
}
}
}
//Only used when initializing a new simulation
public void populate()
{
//Clean the grid
for (int i = 0; i < myArray.GetLength(0); i++)
{
for (int j = 0; j < myArray.GetLength(1); j++)
{
kill(i, j);
}
}
//Distribute Fish randomly
int PosX=0;
int PosY=0;
bool isEmpty=false;
Random rndX = new Random();
Random rndY = new Random();
PosX = rndX.Next(0, TableX);
for (int i = 0; i < initialNumberOfFish; i++)
{
while (!isEmpty)
{
PosX = rndX.Next(TableX);
PosY = rndY.Next(TableY);
if (myArray[PosX, PosY].fishType == fishTypeEnum.empty)
isEmpty = true;
}
myArray[PosX, PosY].fishType = fishTypeEnum.fish;
myArray[PosX, PosY].cellColor = Color.Green;
myArray[PosX, PosY].age = rndX.Next(0, ageToBreedFish);
isEmpty = false;
}
//Distribute Sharks randomly
for (int i = 0; i < initialNumberOfSharks; i++)
{
while (!isEmpty)
{
PosX = rndX.Next(0, TableX);
PosY = rndY.Next(0, TableY);
if (myArray[PosX, PosY].fishType != fishTypeEnum.shark)
isEmpty = true;
}
myArray[PosX, PosY].fishType = fishTypeEnum.shark;
myArray[PosX, PosY].cellColor = Color.Red;
myArray[PosX, PosY].age = rndX.Next(0, yearToStarvation);
myArray[PosX, PosY].ageStarvation = myArray[PosX, PosY].age;
isEmpty = false;
}
}
//A tick is a 'round' where fish swim and breed, sharks hunt and breed
public void tick()
{
bool reproduce;
for (int i = 0; i < myArray.GetLength(0); i++)
{
for (int j = 0; j < myArray.GetLength(1); j++)
{
//If Cell is Shark and hasent moved
if ((myArray[i, j].fishType == fishTypeEnum.shark) && (!myArray[i, j].moved))
{
//Sharks starve
Debug.WriteLine("Hai alter"+myArray[i,j].age);
if (myArray[i, j].ageStarvation > yearToStarvation)
{
kill(i, j);
Debug.WriteLine("Hai verhungert");
}
//If not starved shark hunts
hunt(i, j);
}
else
//If Cell is Fish and hasent moved
if ((myArray[i, j].fishType == fishTypeEnum.fish) && (!myArray[i, j].moved))
{
reproduce = false;
if (myArray[i,j].age >= ageToBreedFish)
{
reproduce = true;
}
move(i, j, fishTypeEnum.fish, reproduce);
}
}
}
//End Tick
for (int i = 0; i < myArray.GetLength(0); i++)
{
for (int j = 0; j < myArray.GetLength(1); j++)
{
myArray[i, j].moved = false;
}
}
}
private void kill(int x, int y)
{
myArray[x, y].age = 0;
myArray[x, y].fishType = fishTypeEnum.empty;
myArray[x, y].cellColor = Color.Blue;
myArray[x, y].moved = false;
}
private void hunt(int x, int y)
{
Random rnd = new Random();
int direction = 0;
int newX = x;
int newY = y;
bool moved = false;
//Check for Fish nearby
direction = fishNearBy(x, y, sharkScent);
Debug.WriteLine("direction: " + direction);
if (direction == 0)
direction = rnd.Next(1, 5);
{
switch (direction)
{
case 1://East. If Index 0 move to TableEnde
if (newY > 0)
{
newY = newY - 1;
moved = true;
}
else
{
newY = TableY - 1;
moved = true;
}
break;
case 2://West. If TableEnde move to 0.
if (newY < TableY - 1)
{
newY = newY + 1;
moved = true;
}
else
{
newY = 0;
moved = true;
}
break;
case 3://North. If top reached try again
if (newX > 0)
{
newX = newX - 1;
moved = true;
}
break;
case 4://South. If bottom reached try again
if (newX < TableX - 1)
{
newX = newX + 1;
moved = true;
}
break;
default:
break;
}//switch
}//if
if (myArray[newX, newY].fishType == fishTypeEnum.fish)
{
Debug.WriteLine("Fisch gefangen");
eatFish(newX, newY);
myArray[x, y].ageStarvation = 0; ;
}
if (moved)
{
Debug.WriteLine("Hai bewegen");
myArray[newX, newY].cellColor = myArray[x, y].cellColor;
myArray[newX, newY].fishType = myArray[x, y].fishType;
myArray[newX, newY].age = myArray[x, y].age + 1;
myArray[newX, newY].ageStarvation = myArray[x, y].ageStarvation + 1;
myArray[newX, newY].moved = true;
//Shark will breed
if (myArray[newX, newY].age >= ageToBreedShark)
{
myArray[newX, newY].age = 0;
myArray[newX, newY].ageStarvation = 0;
myArray[x, y].age = 0;
myArray[x, y].ageStarvation = 0;
myArray[x, y].moved = true;
}
else
{
kill(x, y);
}
}
}
private void eatFish(int x, int y)
{
kill(x, y);
}
private void resetXY(ref int x, ref int y, int a,int b)
{
x = a;
y = b;
}
//Searches for fish. howFar determines how many cells away sharks can find fish
private int fishNearBy(int x, int y, int howFar)
{
int checkX = x;
int checkY = y;
int howClose =howFar+2;
bool fishFound = false;
int direction=0; //1=East, 2=West, 3=North, 4=South
//Array for saving the direction. Distance initialized with howfar(maxlength)
int[] directionArray = new int[5];
for (int i = 0; i < directionArray.GetLength(0); i++)
directionArray[i] = howFar + 1;
//Check East
for (int i = 0; i < howFar; i++)
{
if (checkY < 0)
{
checkY = TableY - 1;
}
if (myArray[x, checkY].fishType == fishTypeEnum.fish)
{
//Update distance
directionArray[1] = i;
fishFound = true;
break;
}
checkY--;
directionArray[1] = 99;
}
resetXY(ref checkX, ref checkY, x, y);
//Check West
for (int i = 0; i < howFar; i++)
{
if (checkY >= TableY)
{
checkY = 0;
}
if (myArray[x, checkY].fishType == fishTypeEnum.fish)
{
//Update distance
directionArray[2] = i;
fishFound = true;
break;
}
checkY++;
directionArray[2] = 99;
}
resetXY(ref checkX, ref checkY, x, y);
//Check North
for (int i = 0; i < howFar; i++)
{
if (checkX <= 0)
{
checkX = 0;
}
if (myArray[checkX, y].fishType == fishTypeEnum.fish)
{
//Update distance
directionArray[3] = i;
fishFound = true;
break;
}
checkX--;
directionArray[3] = 99;
}
resetXY(ref checkX, ref checkY, x, y);
//Check South
for (int i = 0; i < howFar; i++)
{
if (checkX >= TableX)
{
checkX = TableX-1;
}
if (myArray[checkX, y].fishType == fishTypeEnum.fish)
{
//Update distance
directionArray[4] = i;
fishFound = true;
break;
}
checkX++;
directionArray[4] = 99;
}
if (fishFound)
{
//Find the fish that is closest to the shark
for (int i = 1; i < directionArray.GetLength(0); i++)
{
Debug.WriteLine("Fish found, Index: " + Array.IndexOf(directionArray, directionArray[i]) + " Entfernung: " + directionArray[i]);
if (directionArray[i] < howClose)
{
Debug.WriteLine("Fish found, direction: " + Array.IndexOf(directionArray, directionArray[i]));
howClose = directionArray[i];
}
direction = Array.IndexOf(directionArray, howClose);
}
}
return direction;
}
private void move(int x, int y,fishTypeEnum type,bool repro)
{
int newX=x;
int newY=y;
int direction = 4;
bool cellIsFree = false;
bool noFreeCells = false;
int numberOfTries = 0;
int maxTries = 8;
if ((type == fishTypeEnum.fish))
{
while (!cellIsFree && !noFreeCells)
{
direction = rnd.Next(1, 5);//Direction between 1-4
switch (direction)
{
case 1://East. If Index 0 move to TableEnde
if (newY > 0)
{
newY = newY - 1;
}
else
newY = TableY - 1;
break;
case 2://West. If TableEnde move to 0.
if (newY < TableY - 1)
{
newY = newY + 1;
}
else
newY = 0;
break;
case 3://North. If top reached try again
if (newX > 0)
{
newX = newX - 1;
}
break;
case 4://South. If bottom reached try again
if (newX < TableX - 1)
{
newX = newX + 1;
}
break;
}//switch
if (myArray[newX, newY].fishType == fishTypeEnum.empty)
{
cellIsFree = true;
}
numberOfTries++;
if (numberOfTries > maxTries)
noFreeCells = true;
}//if
}//while (!cellIsFree)
//Debug.WriteLine("NewX=" + newX + "NewY=" + newY);
myArray[newX, newY].cellColor = myArray[x, y].cellColor;
myArray[newX, newY].fishType = myArray[x, y].fishType;
myArray[newX, newY].moved = true;
myArray[newX, newY].age = myArray[x, y].age + 1;
//If fish ist not reproducing remove the previous fish
if (!repro)
{
kill(x, y);
}
else
{
myArray[newX, newY].age = 0;
myArray[x, y].age = 0;
myArray[x, y].moved = true;
}
}
private void button2_Click(object sender, EventArgs e)
{
tick();
Canvas.Refresh();
}
private void button1_Click(object sender, EventArgs e)
{
populate();
Canvas.Refresh();
}
}
}
The Cell
class is used to store information about the cells:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace WaTor
{
//Fishtype
enum fishTypeEnum { fish, shark, empty };
class Cell
{
//Color
public Color cellColor { get; set; }
//Age
public int age {get;set;}
//Age used for starvation
public int ageStarvation { get; set; }
//Set&Get Fishtype
public fishTypeEnum fishType {get;set;}
//Check if moved during tick
public bool moved { get; set; }
//Position
public int posX {get;set;}
public int posY {get;set;}
//Constructor
public Cell()
{
cellColor = Color.Blue;
posX = 0;
posY = 0;
age = 0;
ageStarvation = 0;
moved = false;
fishType = fishTypeEnum.empty;
}
}
}
-
1\$\begingroup\$ Welcome to Code Review! To help reviewers give you better answers, please add sufficient context to your question. The more you tell us about what your code does and what the purpose of doing that is, the easier it will be for reviewers to help you. See also this meta question. \$\endgroup\$SuperBiasedMan– SuperBiasedMan2016年02月05日 11:12:53 +00:00Commented Feb 5, 2016 at 11:12
-
\$\begingroup\$ As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does, not what you want to get out of a review. Please see How to get the best value out of Code Review - Asking Questions for guidance on writing good question titles. \$\endgroup\$BCdotWEB– BCdotWEB2016年02月05日 12:30:05 +00:00Commented Feb 5, 2016 at 12:30
-
\$\begingroup\$ Thanks for your feedback! the wator simulation ist what my code does, i mention that in the title. \$\endgroup\$user3136418– user31364182016年02月05日 12:44:30 +00:00Commented Feb 5, 2016 at 12:44
-
1\$\begingroup\$ Ok, I'm a bit tired now but i'll give a full go at this later. In the short term the most obvious problem is highlighted by your comments. look at the populate function. why tell me what its doing. you know what would be better? Populate() { CleanTheGrid(); DistributeFish(); DistributeSharks(); } . read up on SRP (Single Responsibility Principle). same goes for the tick function. quick example: tick() { var currentCell = myArray[i,j]; if(IsShark(currentCell)) updateShark(); else updateFish(); }. try simple refactor into single units of work. then further OOP designs will appear \$\endgroup\$apieceoffruit– apieceoffruit2016年02月05日 15:26:53 +00:00Commented Feb 5, 2016 at 15:26
1 Answer 1
Well, first of all I would suggest you separate your concerns a little more. At the moment you have (most of) your Wa-Tor logic AND your Windows Forms code all together in the same class. Secondly, that class is named Form1! I know Visual Studio is nice when it does things for you, but naming classes isn't it's strong-suit ;)
Separate your presentation from your (domain) logic
As you get into more complex problem spaces, things like decomposition and domain modelling become important steps in translating the problem space into abstractions that you can compose together. Imagine if your Wa-Tor simulation was actually being run on a super computer, in order to produce an accurate model of sea-life populations and breeding patterns in the Pacific Ocean. This would obviously be a massive increase in scope, but shouldn't require any particularly large code changes* (known as refactoring) if your code has been written in a re-usable, decomposed, loosely-coupled way (which is the goal of all OOP coders).
* well, assuming the logic of Wa-Tor is exactly how the Ocean works.
I would suggest that things such as initialNumberOfFish
,
initialNumberOfSharks
, ageToBreedFish
, ageToBreedShark
& yearToStarvation
, etc all belong somewhere other than the (presentation) class Form1
. The same goes for the storage, size and initialization (intitializeArray
) of your array (a better name for which might be "world", or ocean, etc). That class should probably be something along the lines of Simulation
, World
, etc.
The code in intitializeArray
can go into the constructor of that Simulation
class. That constructor can also accept all of the configuration parameters of the simulation (size, age to breed number of fish, etc) as arguments. It can also then call the populate
method.
Also, an additional problem of your presentation code infecting your logical (model) code.. Why does a Cell
have a cellColor
? Colors are something pertaining to display, the Cell
may have a state, status, type or even an isEmpty
property - as these all relate to the simulation (which is what the Cell is part of modelling).
Abstract your behaviours as well as your representations...
- OR : "Don't repeat yourself!"
As @apieceoffruit commented, the Single Responsibility Principle (closely related to the Separation of Concerns I mentioned earlier) should be employed when you are deciding upon the responsibilities for your objects (often called "Don't Repeat Yourself", or DRY). Since objects contain both data (i.e. fields) and behaviour (methods), this principle should be used (as @apieceoffruit said) when designing your methods.
In particular, the populate
method, contains the following code:
//Distribute Fish randomly int PosX=0; int PosY=0; bool isEmpty=false; Random rndX = new Random(); Random rndY = new Random(); PosX = rndX.Next(0, TableX); for (int i = 0; i < initialNumberOfFish; i++) { while (!isEmpty) { PosX = rndX.Next(TableX); PosY = rndY.Next(TableY); if (myArray[PosX, PosY].fishType == fishTypeEnum.empty) isEmpty = true; } myArray[PosX, PosY].fishType = fishTypeEnum.fish; myArray[PosX, PosY].cellColor = Color.Green; myArray[PosX, PosY].age = rndX.Next(0, ageToBreedFish); isEmpty = false; }
Which is then almost copied verbatim for "Distribute Sharks randomly" (obviously with fishTypeEnum.shark
used instead, etc).
This could (and would) be much better Decomposed into a helper method DistributeRandomly
, or (as @apieceoffruit commented) the two methods: DistributeFish
& DistributeSharks
(hopefully which could share some common base helper method, since they're so similar).
There also seems to be something very similar in fishNearBy
, which has four near identical for-loops. The may be able to be abstracted out into a helper method that fishNearBy
can call 4 times, thus saving on code and making refactoring of that logic easier in the future.
Inheritance: (削除) The Answer to All your Problems (削除ここまで) (NOT)
As you mentioned in your question, "inheritance for fish and shark from the cell class", *might* be a good idea. It depends if your behaviours can be made generic enough to apply to both.
For example, in the Wikipedia page about Wa-tor^, it is stated:
Since hunting for fish takes priority over mere movement, the rules for a shark are more complicated: from the adjacent points occupied by fish, select one at random, move there and devour the fish. If no fish are in the neighborhood, the shark moves just as a fish does, avoiding its fellow sharks.
(Emphasis added)
^ Wikipedia attributes this (in a quotation) to Dewdney (1984) "Sharks and fish Wage an ecological War on the toroidal planet Wa-Tor", Scientific American December pg I4—22
From reading this, I would have (as an OOP programmer naturally would,) thought that the move
behaviour of a fish would therefore be something re-usable for a shark.
However, in your code it seems fish move
while sharks hunt
, and no re-use of move
is made at all. This is something you may want to consider refactoring if you were to make Shark
and Fish
classes. For example, the current hunt
method would become an overloaded move
method on Shark
. Shark
would inherit from Fish
(since a shark is a type of fish), and Fish
would contain (a modified version of) the current move
method. This way that code has been made re-usble (one of the goals of an OOP programmer).
Some General Code Smells
In addition to these, there are also just some general things I noticed going through you code, which as you say "is a mess" (well, a small mess anyway).
Multiple Random generators and instance member hiding. You class has
Random rnd = new Random();
, which is then repeated inside ofhunt
(not in itself a big issue, although I don't know why you need a different generator). But insidehunt
, it is also namedrnd
, which means (as Visual Studio would underline it and say) that it ***hides* the instance memberrnd
* (in the class).move
takes afishTypeEnum
argument (namedtype
, probably should be fishType to avoid confusion), and then uses this JUST to check that it is a fish. However, the *ONLY* place thatmove
is called (intick
), this check is already done beforemove
is called! The valuefishTypeEnum.fish
is also hard-coded into the call, making the if-statement to check the value insidemove
completely redundant.This last one isn't really a 'coding' issue, but I'll include it for completeness. In your code, your comment for
tick
states that it represents a "round", however the Wikipedia page about Wa-tor says that these should be called "chronons".
Overall, I think your code is suffering from a lack of clarity of thought when designing. It has a "code first" feel, and as such the data structures and modelling are lacking. Some parts of it feel very procedural in style, other parts just seem more like spaghetti code (lacking a defined style).
I hope I've helped you with the things I've highlighted, comment if there's anything I've said that needs more explanation, or if you'd like me to go into more detail on anything.
-
\$\begingroup\$ just wow! I'll start redesigning the code asap \$\endgroup\$user3136418– user31364182016年02月08日 08:10:05 +00:00Commented Feb 8, 2016 at 8:10
-
\$\begingroup\$ i hope a follow-up question is not against the rules: i seperated the presentation from the logic by creating a new Class Simulation, which contains all the world-parameters. 1. A simulation object is created from the WaTor (renamed Form1) Class. 2. From the simulation object, cell Objects are created. How can the cell objects access variables from the simulation instance that was created from the WaTor Class? My first thought would be to use a pointer to the object, but it doesnt seem to work. \$\endgroup\$user3136418– user31364182016年02月08日 13:54:13 +00:00Commented Feb 8, 2016 at 13:54
-
\$\begingroup\$ Technically yes, but technically you're allowed to edit the question.. So technically, I don't think anybody minds? ;P As you say, from the
Simulation
object,Cell
objects (technically called "instances," but let's not get too technical) are created. That part sounds good so far. My spidey-senses detect a C-programmer.. TheCell
doesn't need to "access variables fromSimulation
" - these can be passed to theCell(foo,bar)
constructor. \$\endgroup\$Tersosauros– Tersosauros2016年02月11日 17:55:31 +00:00Commented Feb 11, 2016 at 17:55