Dots and Lines: The AI

Adding an AI player to the game couldn’t be simpler, really. I add a class called ComputerPlayer and I end up using all the same handlers the “human” player uses: HandleTouchDown, HandleTouchMove and HandleTouchUp.

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
[RequireComponent (typeof (GameView))]
[RequireComponent (typeof (Grid))]
public class ComputerPlayer : MonoBehaviour {
 
 
	private bool moving;
	private Peg origin;
	private Peg destination;
	private Grid grid;
	private GameView game;
	private List<Square> sides0;
	private List<Square> sides1;
	private List<Square> sides2;
	private List<Square> sides3;
 
	private float progress;
	private float speed = 0.05f;
 
	void Start ()
	{
		grid = GetComponent<Grid> ();
		game = GetComponent<GameView> ();
		sides3 = new List<Square> ();
		sides2 = new List<Square> ();
		sides1 = new List<Square> ();
		sides0 = new List<Square> ();
	}
 
	public void Play ()
	{
		origin = destination = null;
 
		SortSquares ();
 
		progress = 0;
 
		if (sides3.Count > 0) {
			PickPegs(sides3[Random.Range(0, sides3.Count)]);
		} else if (sides0.Count > 0) {
			PickPegs(sides0[Random.Range(0, sides0.Count)]);
		} else if (sides1.Count > 0) {
			PickPegs(sides1[Random.Range(0, sides1.Count)]);
		} else if (sides2.Count > 0) {
			PickPegs(sides2[Random.Range(0, sides2.Count)]);
		}
	}
 
	//sort squares by sides
	void SortSquares ()
	{
		sides3.Clear ();
		sides2.Clear ();
		sides1.Clear ();
		sides0.Clear ();
 
		//loop through squares
		for (var column = 0; column < Grid.COLUMNS - 1; column++) {
 
			for (var row = 0; row < Grid.ROWS - 1; row++) {
				var square = grid.squares[column][row];
				if (square.sides == 3) sides3.Add(square);
				else if (square.sides == 2) sides2.Add(square);
				else if (square.sides == 1) sides1.Add(square);
				else if (square.sides == 0) sides0.Add(square);
			}
		}
	}
 
	void PickPegs (Square square)
	{
		var row = square.row;
		var column = square.column;
 
		var pegs = new List<Peg[]> ();
 
		if (!grid.pegs [column] [row].verticalDash.activeSelf) {
			pegs.Add (new Peg[] { grid.pegs [column] [row], grid.pegs [column] [row + 1] });
		}
		if (!grid.pegs [column] [row].horizontalDash.activeSelf) {
			pegs.Add (new Peg[] { grid.pegs [column] [row], grid.pegs [column + 1] [row] });
		}
		if (!grid.pegs [column + 1] [row].verticalDash.activeSelf) {
			pegs.Add (new Peg[] { grid.pegs [column + 1] [row], grid.pegs [column + 1] [row + 1] });
		}
		if (!grid.pegs [column] [row + 1].horizontalDash.activeSelf) {
			pegs.Add (new Peg[] { grid.pegs [column] [row + 1], grid.pegs [column + 1] [row + 1] });
		}
 
		var line = pegs [Random.Range (0, pegs.Count)];
		origin = line [0];
		destination = line [1];
	}
 
	void FixedUpdate ()
	{
 
		if (progress == 0 && origin != null) {
			game.HandleTouchDown (Vector2.zero, origin);
			progress += speed;
		} else {
 
			if (destination != null)
			{
				var touchMove = origin.transform.position + (destination.transform.position - origin.transform.position) * progress;
				progress += speed;
 
				if (progress >= 1)
				{
					game.HandleTouchUp( Vector2.zero, destination );
					origin = destination = null;
 
				}
				else
				{
					if (progress < 0.8f)
						game.HandleWorldTouchMove( touchMove );
					else
						game.HandleWorldTouchMove( touchMove, destination );
				}
			}
		}
	}
}

With every move I collect the squares with 3 sides already drawn, then ones with 2 sides, 1 side and the ones with no sides yet. The strategy for the AI is pretty straightforward, and can be easily adjusted to create less smart players. But for now, if there are no 3 side squares, the AI will look for squares with the least number of sides and add a side to one of those.

In the FixedUpdate loop I animate the drawing of the line, once the logic has a starting peg and a destination peg.

Then all we need are a few changes in the game controller

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
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
 
[RequireComponent (typeof (Grid))]
public class GameView : MonoBehaviour {
 
	public Text player1ScoreText;
 
	public Text player2ScoreText;
 
	private List<Ball> player1Balls;
 
	private List<Ball> player2Balls;
 
	private Peg selectedPeg;
 
	private Peg destinationPeg;
 
	private Grid grid;
 
	private Vector3 screenPoint;
 
	private Vector3 offset;
 
	private int player = 0;
 
	private bool playAgainstComputer = true;
 
	void Awake ()
	{
	}
 
	void Start () 
	{
		player1Balls = new List<Ball> ();
		player2Balls = new List<Ball> ();
		player1ScoreText.text = "0";
		player2ScoreText.text = "0";
		grid = GetComponent<Grid> ();
	}
 
	public void HandleTouchDown (Vector2 touch, Peg pegDown = null)
	{
		if (playAgainstComputer && player == 1 && pegDown == null)
			return;
 
		ClearLines ();
 
		if (selectedPeg != null)
			selectedPeg.Select (false);
 
 
		var peg = pegDown == null ? PegCloseToPoint (touch) : pegDown;
 
		if (peg != null && !peg.IsLocked ()) {
			selectedPeg = peg;
			selectedPeg.Select (true);
 
			screenPoint = Camera.main.WorldToScreenPoint (selectedPeg.transform.position);
			offset = selectedPeg.transform.position - Camera.main.ScreenToWorldPoint (new Vector3 (touch.x, touch.y, screenPoint.z));
		} else {
			ClearLines ();
		}
	}
 
	public void HandleWorldTouchMove (Vector2 touch, Peg peg = null)
	{
		if (selectedPeg == null)
			return;
 
		selectedPeg.DrawLineTo (touch);
 
		destinationPeg = null;
 
		foreach (var column in grid.pegs) {
			foreach (var p in column)
			{
				if (p != selectedPeg) p.Select(false);
			}
		}
 
		if (peg != null && grid.CanDrawLineToPeg (selectedPeg, peg))
			peg.Select (true);
	}
 
	public void HandleTouchMove (Vector2 touch)
	{
		if (playAgainstComputer && player == 1)
			return;
 
		if (selectedPeg == null)
			return;
 
		//draw line
		Vector3 cursorPoint = new Vector3(touch.x, touch.y, screenPoint.z);
		Vector3 cursorPosition = Camera.main.ScreenToWorldPoint(cursorPoint) + offset;
		selectedPeg.DrawLineTo (cursorPosition);
 
		destinationPeg = null;
 
		foreach (var column in grid.pegs) {
			foreach (var p in column)
			{
				if (p != selectedPeg) p.Select(false);
			}
		}
 
		var peg = PegCloseToPoint (touch);
 
		if (peg != null && grid.CanDrawLineToPeg (selectedPeg, peg))
			peg.Select (true);
 
	}
 
	public void HandleTouchUp (Vector2 touch, Peg pegUp = null)
	{
		if (playAgainstComputer && player == 1 && pegUp == null)
			return;
 
		ClearLines ();
 
 
		if (selectedPeg == null) {
 
			return;
		}
		var peg = pegUp == null ? PegCloseToPoint (touch) : pegUp;
 
		if (peg != null && grid.CanDrawLineToPeg (selectedPeg, peg)) {
 
			destinationPeg = peg;
			selectedPeg.SetLineToPeg (destinationPeg, player);
			grid.AddLine (selectedPeg, destinationPeg);
 
			var newCircles = grid.AddCircle (destinationPeg);
 
			foreach (var circle in newCircles)
			{
				if (player == 0) 
				{
					player1Balls.Add(circle);
				}
				else
				{
					player2Balls.Add(circle);
				}
 
				circle.SetBallToPlayer (player);
			}
 
 
			if (newCircles.Count == 0) player = player == 0 ? 1 : 0;
 
 
			player1ScoreText.text = "" + player1Balls.Count;
			player2ScoreText.text = "" + player2Balls.Count;
 
			if (player1Balls.Count + player2Balls.Count == (Grid.COLUMNS - 1) * (Grid.ROWS - 1))
			{
				Debug.Log ("GAME OVER");
				if (player1Balls.Count > player2Balls.Count) Debug.Log ("PLAYER 1 WON");
				else Debug.Log ("PLAYER 2 WON");
			}
		}
 
		selectedPeg.Select (false);
		selectedPeg.DisableLine ();
		if (destinationPeg != null) destinationPeg.Select (false);
		selectedPeg = null;
		destinationPeg = null;
 
		if (playAgainstComputer && player == 1) {
			StartCoroutine("ComputerPlays");
		}
	}
 
	IEnumerator ComputerPlays ()
	{
		yield return new WaitForSeconds (0.25f);
		GetComponent<ComputerPlayer> ().Play ();
	}
 
	private void ClearLines ()
	{
		foreach (var column in grid.pegs) {
			foreach (var peg in column)
			{
				peg.DisableLine();
			}
		}
	}
 
	private Peg PegCloseToPoint (Vector2 point)
	{
		var t = Camera.main.ScreenToWorldPoint (point);
		t.z = 0;
 
		int c = Mathf.FloorToInt ((t.x - Grid.OFFSET_X + ( Peg.size * 0.5f )) / Peg.size);
		if (c < 0) return null;
		if (c >= Grid.COLUMNS) return null;
 
		int r =  Mathf.FloorToInt ((Grid.OFFSET_Y + ( Peg.size * 0.5f ) - t.y )/  Peg.size);
		if (r < 0) return null;
		if (r >= Grid.ROWS) return null;
 
		return grid.pegs [c] [r];
	}
}

Instead of switching to another player, I now block the game until the AI is through. The AI starts its move with a slight delay of 0.25 seconds, so the player has a good chance of seeing what his opponent is doing.

And that’s it. Try it. The game is quite fun to play.

You can download the source here.