School Project Fun

So in my final semester for my 39-year bachelor’s, one of the things I have to do is build an SOS game. It’s a grid game where players try to spell SOS. A bit harder than Tic-Tac-Toe, but not too bad. We don’t have any restrictions on language, so I’m building it with SwiftUI, since the only thing I’m more familiar with is ASOC, and that seems mean to do to a prof who really only groks Java.

The project itself is at https://github.com/johncwelch/SOS-Swift, and so far, I have the basic window setup done, i.e. the controls for the stuff and some very basic unit tests. I’ll be adding to this post as I go along as a way of documenting some of the “fun” bits of SwiftUI, especially dealing with Apple’s “If you actually need docs, go write C# you loser” attitude towards proper documentation

Spare me the arguments, Apple’s Developer Docs are awful. Pages of how to init a control, nothing on the properties of said control, barely more on the methods. Let me put it this way, it is possible, without excessive work for someone basically new to C# to, using only MS’s official dev docs, build an application in C#. It’d be a bit of work, but the docs are complete and the sample code is solid. It would be impossible for someone to do the same with Swift(UI) using only Apple’s developer documentation. It’s awful, just bookmark Hacking With Swift, save yourself some pain.

Initial Setup

I’m not going to do a line by line thing here, just talk about some of the more interesting things. The first thing I did was lost that “default” VStack so that the HStack that all my game control bits are in would stretch the width of the window via the .frame(maxWidth: .infinity) property. Also, WHY DOES APPLE REFUSE TO DOCUMENT PROPERTIES WORTH A SHIT???

Another early thing I do, that I do to about every app I write is in the AppNameApp.swift file, I add in:

class AppDelegate: NSObject, NSApplicationDelegate {
     func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
          return true
     }
}

and then in the @main app struct I add: @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate, this way, when I close the application window, it auto-quits. Way handy.

Another thing I did was start taking code partitioning more seriously, so the only thing in my ContentView file are things that have to be there. Everything else, like radio button setup structs, my viewmodifiers, those are all in SOS_SwiftApp.swift. THAT file may get a bit long, but it will be nicely modular.

ViewModifiers are really handy, they’re basically stylesheets for things. For example, i have a few Text fields, that have really similar needs in terms of font, font weight, text selection enabled, and few more that have those same properties, but also need the exact same frame sizes. So, we create two structs:

struct basicTextModifier: ViewModifier {
     func body(content: Content) -> some View {
          return content
          .font(.body)
          .fontWeight(.bold)
          .frame(width: 120.0,height: 22.0,alignment: .leading)
          .textSelection(.enabled)
     }
}
struct basicTextModifierNoFrame: ViewModifier {
	func body(content: Content) -> some View {
		return content
			.font(.body)
			.fontWeight(.bold)
			.textSelection(.enabled)
	}
}

The difference is pretty easy to see, but those let me replace all the formatting code across multiple Text fields with .modifier(basicTextModifier()) and .modifier(basicTextModifierNoFrame()) which cleans up the code in ContentView nicely.

The work to create radio buttons in SwiftUI is a bit twee (although not as twee as pasting multiline text into a wordpress block. Jesus wept but that shit is incompetently done.) First you build the definition struct:

struct gameTypeRadioButtonView: View {
	var index: Int
	@Binding var selectedIndex: Int

	var body: some View {
		//in swiftUI ALL BUTTONS ARE BUTTONS
		Button(action: {
			selectedIndex = index
		}) {
			//this sets up the view for the basic button. You define it here once, then implement it in
			//the main view
			HStack {
				//literally build the buttons this way because fuck if I know, this UI by code shit is
				//stupid
				Image(systemName:  self.selectedIndex == self.index ? "largecircle.fill.circle" : "circle")
					.foregroundColor(.black)
				//set the label for each one
				if index == 1 {
					Text("Simple")
				} else if index == 2 {
					Text("General")
				}

			}
			//.padding(.leading, 20.0)
		}
	}
}

Then in ContentView, you implement them:

VStack(alignment: .leading) {
			    Text("Game Type:")
					.modifier(basicTextModifier())
					.accessibilityLabel("Game Type label")
					.accessibilityIdentifier("gameTypeLabel")
				    //this actually draws the buttons
				gameTypeRadioButtonView(index: 1, selectedIndex: $gameType)
				gameTypeRadioButtonView(index: 2, selectedIndex: $gameType)
					//
			}
			.padding(.leading, 20.0)

So it works, but it’s a bit weirder than other languages where you just define the radio group, then each button in that group does what it should. Pickers are similarly weird in that there’s one picker, and what it looks like is defined by the .pickerStyle”

Picker("Board Size", selection: $boardSize) {
	Text("3").tag(3)
	Text("4").tag(4)
	Text("5").tag(5)
	Text("6").tag(6)
	Text("7").tag(7)
	Text("8").tag(8)
	Text("9").tag(9)
	Text("10").tag(10)
}
//picker properties
//padding has to be separate for each dimensions
	.modifier(basicTextModifierNoFrame())
	.frame(width: 140.0,alignment: .center)
	.padding(.top,2)
	.accessibilityLabel("Board Size Dropdown")
	.accessibilityIdentifier("boardSizeDropdown")
	//makes it look like a dropdown list
	.pickerStyle(MenuPickerStyle())
	//this is how you initiate actions based on a change event
	.onChange(of: boardSize) {
	        boardSizeSelect(theSelection: boardSize)
	}

Obviously I’m using state vars for this stuff, which as we’ll see when I add in the grid, will make our live very easy by reducing the amount of work needed to resize the grid on the fly to essentially zero:

@State var gameType: Int = 1
@State var boardSize: Int = 3
@State var bluePlayerType: Int = 1
@State var redPlayerType: Int = 1
@State var currentPlayer: String = "Blue"

Unit Tests!

So right now, I do not have a lot of Unit Tests in my code. This is less “I hate unit tests” and more “I spend all my (coding) time doing things where Unit Tests are not a big deal or they create dependencies. Unit Tests in PowerShell and AppleScript are very, very, different than in things like Swift or C#. Not conceptually, but how you implement them. So for right now, they’re all very basic, like does the app launch, does this button do what it should, etc. Does the button even click?

I’m having to learn about Unit Tests on the fly, they’re kind of an opaque topic, and have a lot of “well, clearly you already know so why do you need documentation” infecting them. They’ll show up later.

The Game Grid

While I like to bag on how weird SwiftUI can be and how execrable Apple’s documentation is, there are times SwiftUI is great. Like managing the game grid. This has to be a grid between 3×3 and 10×10 in size. So kind of a pain in the ass if you’re going to do this manually. Luckily, I’m not doing this manually. The grid is one state variable: @State var boardSize: Int = 3 and then the grid itself:

  HStack(alignment: .top) {
			Grid (horizontalSpacing: 0, verticalSpacing: 0){

				//the view (grid) will refresh if you change the state var gridSize,
				//but, you have to include the id: \.self for it to work right, because
				//of how swift handles this. Note, you don't use the id, 
				//this is just telling the view what's going on.

				//row foreach
				ForEach(0..<boardSize, id: \.self) { row in
					GridRow {
						//column foreach
						ForEach(0..<boardSize, id: \.self) {col in
								//put a rectangle in each grid space
								//the overlay is how you add text
								//the border is how you set up grid lines
								//the order is important. if foreground color comes after
								//overlay, it covers the overlay
							Rectangle()
								.foregroundColor(.teal)
								.overlay(Text("\(row),\(col)").fontWeight(.heavy))
								.border(Color.black)
						}
					}
				}
			}
		}

That’s the entire grid, and because I’m basing the size on the boardSize state var, whenever that’s changed in the picker, the grid is refreshed, and redrawn to the new size, and I do…nothing. The next step on the game is putting buttons in each grid, but before that, I wanted to add dynamic score fields.

Score Fields

There are two types of games: simple and general. For a simple game, it’s over whenever someone finally makes “SOS” or the board is filled. So there’s no score, it’s sudden death. For a general game, it goes until the board is filled, and whomever made the most SOS’s wins. So general needs a score, simple does not. Therefore, the score fields should only show up when the game type is general. State vars to the rescue. First, we create the ints for the score, @State var bluePlayerScore: Int = 0 and @State var redPlayerScore: Int = 0. Then we create an Hstack for the score that will live at the bottom of the column with the other respective player info, with a specific if statement:

 HStack(alignment: .center) {
					//in an if statement, we don't use the $varname, just the varname
					if gameType != 1 {
						Text("Blue Score")
						Text(String(bluePlayerScore))
						     .frame(width: 25, height: 22, alignment: .center)
					}
				}
				.frame(width: 105, height: 22, alignment: .leading)

So whenever the gameType radio button is set to simple, the score fields are not shown. When its set to general, they are:

I do zero “onClick()”-type coding, that’s all for free. Sometimes SwiftUI does not suck.

BUTTONS

Oh christ, this one wanted to kill me because the obvious solution of frame maxwidth and maxheight both set to infinity only half-worked, for the width. So stupid. So I need to be able to dynamically set the width and height of the button via the text fram, but how? Turns out, there is functionality that’s actually pretty easy, and designed to do just this kind of thing: GeometryReader. Basically, GeometryReader lets you get a bunch of well, geometric info from a view, and what is a grid cell but a view? Right. So i want to keep my rectangle for now, and put the button on top of it, and we put the entire thing inside GeometryReader{} in the inner loop that builds the grid:

 GeometryReader { gridCellSize in
		Rectangle()
			.foregroundColor(.teal)
			.overlay(Text("\(row),\(col)").fontWeight(.heavy))
			.border(Color.black)

		Button {

		} label: {
			Text("")
				.frame(width: gridCellSize.frame(in: .global).width,height: gridCellSize.frame(in: .global).height)
		}
	}

That does exactly what we need, no muss, no fuss. Next step, getting the buttons to cycle between values on a click, but that should be easy. Well, easier.

NARRATOR VOICE: It was NOT easier

Advanced Button Tedium

This is where I realized that in terms of SwiftUI documentation online, it’s all either “This is a button, isn’t it cooooool” or it’s all hyperadvanced shit for people who have been coding Swift since it first came out. The in-between stage is naught but crickets. As well, a lot of the descriptions of things are really clearly written for…not n00bs. The various explanations of how @State et al work? Dude, I just guessed and prayed.

Luckily, and I mean that, the folks in the Hacking With Swift forums and some very kind folks on Mastodon took way too much of their time to help out, and so I was able to dope it out. The gist of it is, when I click on a button, I need it to cycle between “”, “S” and “O”. I need it to do some other things, but those are pretty easy. This basic thing turned out to be shockingly hard, and really counterintuitive, since as near as I can tell, there’s no way to determine what order things happen in with SwiftUI outside of specific things like button clicks. Layout, on appear, on change, it’s all kind of “it happens when it happens”, which is really not okay.

I ended up making some core changes to my setup. First, we have our Cell class:

 @Observable
class Cell: Identifiable {
	let id = UUID()
	var title: String = ""
	var buttonDisabled: Bool = false
	var index: Int = 0
	var xCoord: Int = 0
	var yCoord: Int = 0
	var backCol: Color = .gray
	//var disabled: Bool = false
}

This sets up a few things like the title, the background color, etc. I don’t really understand why it has to be @Observable, only that it does. Okay. Also, I learned that if you don’t explicitly import Observation, SOME of Observable works, SOME does not. Again, THIS IS NOT OKAY. If I should import Observation to really use @Observable, then for Christ’s sake, throw build error and tell me. It’s two words, not a big deal to add to the top of the file.

Next we have our second observable class, Game, which creates an array of Cell, created by buildStructArray that by the time we’re done, creates the objects we attach to our grid of buttons. From my POV, there is a lot of magic here, but it works, so cool. I’m in IT, magic is a thing. Game Class:

 //class that is a collection of cells that are attached to the buttons in contentview
@Observable class Game {
	//this sets the size of the grid, passsed when creating the array
	var gridSize: Int {
		didSet {
			//builds the array based on the int passed in which is set in the picker
			gridCellArr = buildStructArray(theGridSize: gridSize)
		}
	}

	//create the array var
	var gridCellArr: [Cell] = []

	//initialization
	init(gridSize: Int) {
		self.gridSize = gridSize
		self.gridCellArr = buildStructArray(theGridSize: gridSize)
	}
}

buildStructArray:

 //this builds an array of Cell that gets attached to each button in the game. It's kind of important
//takes in the grid size set in the UI, and then returns an array of cells
func buildStructArray(theGridSize: Int) -> [Cell] {
	var myStructArray: [Cell] = []
	let arraySize = (theGridSize * theGridSize) - 1
	for i in 0...arraySize {
		myStructArray.append(Cell())
		myStructArray[i].index = i
		myStructArray[i].backCol = .gray
	}

	return myStructArray
}

That’s all set up initially in a state var for our 3×3 grid we have on launch: @State var theGame = Game(gridSize: 3)

Because of this, we also modified our picker:

 Picker("Board Size", selection: $theGame.gridSize) {
	    Text("3").tag(3)
	    Text("4").tag(4)
	    Text("5").tag(5)
	    Text("6").tag(6)
	    Text("7").tag(7)
	    Text("8").tag(8)
	    Text("9").tag(9)
	    Text("10").tag(10)
}
		//picker properties
		//padding has to be separate for each dimensions
		.modifier(basicTextModifierNoFrame())
		.frame(width: 140.0,alignment: .center)
		.padding(.top,2)
		.accessibilityLabel("Board Size Dropdown")
		.accessibilityIdentifier("boardSizeDropdown")
	    //makes it look like a dropdown list
		.pickerStyle(MenuPickerStyle())
		//this is how you initiate actions based on a change event
		//test func to show size of board based on selection
		.onChange(of: theGame.gridSize) {
        				    boardSizeSelect(theSelection: theGame.gridSize)
		}

boardSizeSelect(theSelection: theGame.gridSize)is still just a test function to validate the picker.

I also had to modify the ForEach statements: ForEach(0..<theGame.gridSize, id: \.self) { for this change. On the upside, by setting the title to use theGame properties and setting the .frame option there, I no longer needed the Rectangle() code, so cool. New label and onAppear code:

 label: {
		//set the text of the button to be the title of the button
       Text(theGame.gridCellArr[myIndex].title)
                 //set the font of the button text to be system with a
                 //size of 36, a weight of heavy, and to be a serif font
                 .font(.system(size: 36, weight: .heavy, design: .serif))

				//this ensures the buttons are always the right size
				.frame(width: gridCellSize.frame(in: .global).width,height: gridCellSize.frame(in: .global).height, alignment: .center)
}
 //styles button. Since I only have to do this once, here, there's no
 //real point in building a separate button style
 //note that .background is necessary to avoid weird button display errors
 .foregroundStyle(buttonTextColor)
 //this allows the button color to change on commit
 .background(theGame.gridCellArr[myIndex].backCol)
 .border(Color.black)
 //once a move is committed, buttonDisabled is set to true, and the button is
 //disabled so it can't be used again
 .disabled(theGame.gridCellArr[myIndex].buttonDisabled)
 .onAppear(perform: {
			//this has each button set its own coordinates as it appears
			//which is IMPORTANT later on
			theGame.gridCellArr[myIndex].xCoord = col
			theGame.gridCellArr[myIndex].yCoord = row
})

The onAppear sets the coordinates of the button which we’ll need to calculate if SOS has been built. Also, we added .id(theGame.gridSize) to the outer ForEach loop, which insured that all buttons were completely redrawn when the grid size changed. Without that, it would only draw the “new” buttons and all the “old” ones would be blank, with coordinates of 0,0, which was not okay.

However, now I could start doing something besides dicking with buttons. Mind you, this shit took over a week to figure out. Have I mentioned Apple’s developer docs are shit? Because they are. Inexcusably so.

Next up: the beginnings of “Commit Move” and “New Game” is finally complete!

More On Unit Tests

Well not as such. Adding unit tests has been oddly difficult for two reasons, one my doing, one not. The my doing is i don’t write for unit tests. Most of my functions have multiple parameters in and tuples coming out. Some of them are on the long side. But as a sysadmin, most of my coding has been for my specific needs, an audience of one, and what I’m coding for is really straightforward. Also, most of my functions aren’t returning bools.

The other…I swear there is a disease in the Swift world where no one comprehends the needs of newer devs. Every example of Unit Tests in SwiftUI has been along two lines: Either buttons are the only thing one ever needs to test or everyone is evidently testing custom protocols that override every class in Swift(UI). Meanwhile, I’m out here trying to track a variable and there’s nothing on that.

Anywhere.

However, I was able to get some things working, like testing for control states after clicking a button. The clicking a button is just recording actions for the app (god, I LOVE the recording feature.) The the rest is:

  1. Create an accessibility label for the control you want to test
  2. use that in an XCTAsserttrue/false statement

for example, when I click on the “New Game” button, there’s a few controls that need to be enabled that may or may not be currently enabled depending on what’s going on. Here’s the test:

 func testNewGameButton() {
		let SOSApp = XCUIApplication()
		//launch application for button click test
		SOSApp.launch()
		let myCommitButton = SOSApp.buttons["commitButton"]
		let myGameTypeSimpleButton = SOSApp.buttons["gameTypeSimple"]
		let myGameTypeGeneralButton = SOSApp.buttons["gameTypeGeneral"]
		let myBluePlayerTypeHuman = SOSApp.buttons["Blue Player Human"]
		let myBluePlayerTypeComputer = SOSApp.buttons["Blue Player Computer"]
		let myRedPlayerTypeHuman = SOSApp.buttons["Red Player Human"]
		let myRedPlayerTypeComputer = SOSApp.buttons["Red Player Computer"]
		let myCurrentPlayer = SOSApp.staticTexts["Current Player"]
		//click the button
		SOSApp.windows["SwiftUI.ModifiedContent<SOS_Swift.ContentView, SwiftUI._FlexFrameLayout>-1-AppWindow-1"].buttons["New Game"].click()
		//states that should be a specific way after clicking new game:
		XCTAssertFalse(myCommitButton.isEnabled)
		XCTAssertTrue(myGameTypeSimpleButton.isEnabled)
		XCTAssertTrue(myGameTypeGeneralButton.isEnabled)
		XCTAssertTrue(myBluePlayerTypeHuman.isEnabled)
		XCTAssertTrue(myBluePlayerTypeComputer.isEnabled)
		XCTAssertTrue(myRedPlayerTypeHuman.isEnabled)
		XCTAssertTrue(myRedPlayerTypeComputer.isEnabled)
	}

It looks like there’s a lot going on but there really isn’t:

  1. Set SOSApp to XCUIApplication, namely the app we’re testing
  2. Launch SOSApp
  3. Set up vars for the various buttons (regular and radio alike, SwiftUI doesn’t care, they’re all buttons. That’s good and annoying in equal parts) and text fields (staticTexts in SwiftUI Unit Test-ese)
  4. There’s the statement that clicks the new game button. The name used, “New Game” is the label on the button, that doesn’t have an accessibilityLabel. If I create one for that button, then the accessibilityLabel, not the button label would be used in that statement
  5. then we just check for the enabled state of things.

Note that I don’t test for the staticText item. Because I’ve yet to figure out how to. Because in all the online tutorials, no one can be arsed to show something so simple, and before you ask, no, Apple’s documentation is of zero use here. But then, Apple’s documentation is always of zero use, so no surprise there.

Really, I’m going to talk about commit move et al soon. promise.

What Happens When You Click The Button?

As it turns out, to quote “Caligula”, everything and nothing. Well, more everything. Sort of. Meh, how many chances to you get to quote a Tinto Brass classic?

Anyway, when you click the button, first there’s a check that you’re not trying to cheat by setting the player type to computer and then clicking. Honestly, there’s exactly one way that can happen, which is the starting player, always Blue is set to computer and you click the button before you click “start game” which I’ll get into in a minute. (it’s a hack, but it works). The no cheating checks:

 if (currentPlayer == "Blue") && (bluePlayerType == 2) {
       return
}

if (currentPlayer == "Red") && (redPlayerType == 2) {
      return
}

if the conditions of either if are met, nothing happens when you click. Next, we set the index of the last button clicked. This happens twice here, and I really only need once, but for now, it stays, I’ll deal with that during the “make it pretty” sprint.

 lastButtonClickedIndex = theGame.gridCellArr[myIndex].index

Now we’re going to call the aptly named buttonClickStuff(for: theGame.gridCellArr[myIndex].index). myIndex is defined before we get to the button click as:

let myIndex = (row * theGame.gridSize) + col

buttonClickStuff() takes the title of the button that was clicked, which can be “”, “S”, or “O”, the game array itself, the current player, and an array of currently unused buttons, and it returns a tuple consisting of a string value for title, a bool for the new commit button status, and a string for the current player. the call looks like:

let theTuple = buttonClickStuff(for: theGame.gridCellArr[myIndex].index, theTitle: theGame.gridCellArr[myIndex].title, myArray: theGame, myCurrentPlayer: currentPlayer, myUnusedButtons: arrayUsedButtonsList)

Within buttonClickStuff(), there’s a few critical things going on. First, we make mutable vars of the Commit button status, the title, and the current player, since as passed, those values are not mutable and we’ll need to…mutable them.

var theCommitButtonStatus: Bool = false
var theCellTitle: String = ""
var theCurrentPlayer: String = ""

Next we have a switch-case that is used to set the title of the button. This function runs every time you click the button, so this is how we rotate between the three titles;

 switch theTitle {
		//button is current blank, click sets it to "S"
		case "":
			theCellTitle = "S"
			theCommitButtonStatus = false
			//disable all other enabled buttons but the one that you're clicking
			disableOtherButtonsDuringMove(myGridArray: myArray, currentButtonIndex: myIndex)
		//button is currently "S", click sets it to "O"
		case "S":
			theCellTitle = "O"
			theCommitButtonStatus = false
			//disable all other enabled buttons but the one that you're clicking
			disableOtherButtonsDuringMove(myGridArray: myArray, currentButtonIndex: myIndex)
		//button is currently "O", click sets it to ""
		case "O":
			theCellTitle = ""
			theCommitButtonStatus = true
			//enable the other buttons that should be enabled since the button you just clicked
			//is now blank and you can click the other buttons to change your move
			enableOtherButtonsDuringMove(myGridArray: myArray)
		default:
			print("Something went wrong, try restarting the app")
	}

As we want all the other buttons to be disabled if the title of the button being clicked is not “”, to prevent cheating, we call disableOtherButtonsDuringMove() and pass it the array we passed to buttonClickStuff() and the index of the button being clicked. That function looks like:

 func disableOtherButtonsDuringMove (myGridArray: Game, currentButtonIndex: Int) {
	//iterate through the array
	for i in 0..<myGridArray.gridCellArr.count {
		//look for buttons that are not the current button and are not already disabled
		if !myGridArray.gridCellArr[i].buttonDisabled && i != currentButtonIndex {
			myGridArray.gridCellArr[i].buttonDisabled = true
		}
	}
}

It’s a pretty brute force function, but it works and ensures that only the current button being clicked is usable if the title of that button is S or O. If clicking the button returns the title back to “”, the last none-default option in the switch-case, we call enableOtherButtonsDuringMove() and pass it the game array we passed into buttonClickStuff():

 func enableOtherButtonsDuringMove (myGridArray: Game){
	for i in 0..<myGridArray.gridCellArr.count {
		if myGridArray.gridCellArr[i].title == "" {
			myGridArray.gridCellArr[i].buttonDisabled = false
		}
	}
}

Again, a dead stupid function that works and is fast. And since Game is a class, and class is always passed by reference, it just works. W00t!

The default case prints an error to the console. I’ve yet to hit it.

Once we’re out of the switch-case, we change the current player var:

 if myCurrentPlayer == "Blue" {
		theCurrentPlayer = "Red"
} else {
		theCurrentPlayer = "Blue"
}

I am not a clever programmer, I am not a programmer at all, I’m a sysadmin who sometimes has to write code. The way I write code shows that, but sometimes folks need a reminder.

Finally, we build our return tuple and well, return it:

 let theReturnTuple = (myTitle: theCellTitle, myCommitButtonStatus: theCommitButtonStatus, myCurrentPlayer: theCurrentPlayer)
return theReturnTuple

This dumps us back into ContentView, where we set the title of the button to the title returned by the tuple. This can be done back in buttonClickStuff(), and probably will be in the “make it pretty” sprint.

 theGame.gridCellArr[myIndex].title = theTuple.myTitle

Next, we use the Commit button status to set buttonBlank, which either enables the “Commit Move” button (new title is S or O) or sets it to/keeps it disabled (title is “”):

 buttonBlank = theTuple.myCommitButtonStatus

And since the label of the button is also using the same title as what we set with the return from buttonClickStuff(), that’s set automagically by setting the title of the button to theTuple.myTitle

Next up: committing the move, and oh there is some shit happing with that.

Computer Players

Yes I know I said commit move, but I just finished all the code for computer players which has a massive effect on commit, so we deal with the first parts of that.

With computer players, there’s three scenarios:

  1. Blue Computer, Red Human
  2. Blue Human, Red Computer
  3. Blue Computer, Red Computer

Since Blue always goes first on launch or for a new game, I needed a way to set blue to computer that didn’t start as soon as I changed the radio button, because that would be so annoying. I didn’t need it for Red, since Red never makes the first move. This is also not an issue once a move has been made regardless because once that happens, the player type radio buttons are disabled. So for Blue, when you click the “Computer” radio button, two things happen. We show the Start Game button and we disable all the buttons on the grid and make sure they’re all blank. (again, this can ONLY happen when it’s a new game AND Blue is computer. We don’t care for Red, Red never starts a game):

.onChange(of: bluePlayerType) {
			//we only hide when BOTH players are human
			if (bluePlayerType == 1) && (redPlayerType == 1) {
					showStartGameButton = false
					//if they change blue back to human, re-enable all the buttons
					enableOtherButtonsDuringMove(myGridArray: theGame)
					//modifying this because we don't need the start game button for the red player
					//only the blue, since blue ALWAYS goes first in a new game.
			} else if (bluePlayerType == 2) {
					showStartGameButton = true
					disableAllButtonsForBlueComputerPlayerStart(myGridArray: theGame)
			}
}

and here’s disableAllButtonsForBlueComputerPlayerStart():

func disableAllButtonsForBlueComputerPlayerStart (myGridArray: Game) {
	for i in 0..<myGridArray.gridCellArr.count {
		myGridArray.gridCellArr[i].title = ""
		myGridArray.gridCellArr[i].buttonDisabled = true
	}
}

It’s a brute force function, but it works. enableOtherButtonsDuringMove() is similarly simple:

func enableOtherButtonsDuringMove (myGridArray: Game){
	for i in 0..<myGridArray.gridCellArr.count {
		if myGridArray.gridCellArr[i].title == "" {
			myGridArray.gridCellArr[i].buttonDisabled = false
		}
	}
}

Since all buttons have a title of “” when it’s called here, this works. (We also use this function in a few other places, so not changing it was important.

So now the Start Button. First, we check to see if it should even exist:

if showStartGameButton {

Now we set up a buttonTitle var that’s an empty string (which stands a high percentage of going away, since we ended up not using it, but that’s for the next sprint), and we set up our button code (the comments are important and save me from having to narrate the thing here:

 //since we are effectively starting the game, and we don't actually click the commit move
//button, we replicate much of that here
//disable things
gamePlayerTypeDisabled = true

//since this only works on the first move, which is always blue, there's no point in caring
//about red, but it literally takes half a line of code to set it up, so why not?
if ((currentPlayer == "Blue") && (bluePlayerType == 2)) || ((currentPlayer == "Red") && (redPlayerType == 2)) {

	let theStartGameTuple = startGame(myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)
buttonTitle = theStartGameTuple.myButtonTitle
lastButtonClickedIndex = theStartGameTuple.myButtonToClick
}

And what does startGame() do? Literally starts the first computer move:

func startGame(myUnusedButtons: [Int], myGridArray: Game, myCurrentPlayer: String, myArrayUsedMemberCountdown: Int) -> (myButtonTitle: String, myButtonToClick: Int){
	//create title array, shuffle it, and set the first element to be the new button title
	let buttonTitles = ["S","O"]
	//shuffle the array to get as random a result as we can
	let shuffledArray = buttonTitles.shuffled()
	//always use the first item in this array
	let buttonTitle = shuffledArray[0]
	//get the size of the unused button array
	let sizeOfUnusedButtons = myUnusedButtons.count
	//get a random number from 0 to last element of the array
	//get the Index of the button we want to click
	let buttonToClickIndex = Int.random(in: 0..<sizeOfUnusedButtons)
	//this is necessary to avoid some gnarly out of range errors. Basically, the button uses the raw number to determine which one is getting modified
	//so if the size of the array is say 6, then index 5 is a valid choice. But, if there's no button with an index of 5, BOOM. So this ensures the value
	//of the array at the index is what is used, not the raw index itself:
	//if index is [0,1,3,4,6,7], the value at index 5 is 7, but there's now no available button with an index of 5, so if we use 5, it's bad.
	//this prevents that.
	let buttonToClick = myUnusedButtons[buttonToClickIndex]
	//we'll need to set the title. we can ignore the commit button status for now, we're not using it
	//once we set the title, we call commitMove() once we exit this function
	//set the title
	myGridArray.gridCellArr[buttonToClick].title = buttonTitle
	//build the return. Note, we don't seem to use the button title we return, think about dumping it.
	let computerPlayerTuple = (myButtonTitle: buttonTitle, myButtonToClick: buttonToClick)
	//return the tuple
	return computerPlayerTuple
}

The array index thing in there drove me spare for a hot minute, but yeah, it’s working. Now, we have a button with a title, and the index that was “clicked”, the same as if we’d manually set a button to S or O and clicked the Commit Move button. So this first move runs along fairly simply, in that as this code only runs for the first move, we never have to check to see if the game is over/won:

 //so now we have a title and a button clicked, let's call commit move
let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

arrayUsedButtonsList = theCommitTuple.myUnusedButtonArray
arrayUsedMemberCountdown = theCommitTuple.myCountDownInt

//this is true if that commit move created an SOS
var SOSFlag = theCommitTuple.mySOSFlag
//used for incrementing scores in general game because you can have one move create multiple SOS's
var SOSCounter = theCommitTuple.mySOSCounter
//even though we didn't "click" commit move, we want the commit button to be disabled
buttonBlank = true
//once we start the game regardless of how, we don't need start game to be usable
//since we can't change the player type mid game anyway
disableStartGameButton = true
//no one has won, and general game and SOS, increment score. note a win is impossible
//for start button since it can only be used as the first move
if (gameType == 2) && (SOSFlag) {
    	var incrementScoreTuple = incrementScore(myCurrentPlayer: currentPlayer, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore, mySOSCounter: SOSCounter)
		redPlayerScore = incrementScoreTuple.myRedPlayerScore
		bluePlayerScore = incrementScoreTuple.myBluePlayerScore
}
//we don't check for winning game here, the button isn't enabled for that
//get is game over/player won flag

//change the player
currentPlayer = changePlayer(myCurrentPlayer: currentPlayer)

//if both players aren't computer, we want to bug out here. This button only gets clicked if
//both players are computer, so we can cheat with this:
//red is a human, bug out of this code
if (currentPlayer == "Red") && (redPlayerType == 1) {
		return
}

Okay, so we’ve handled the state where Blue is a computer and we clicked Start Game. There’s only one other condition we care about in this button code, and that is if Red is ALSO a computer, i.e. computer v. computer. This is where we have a while loop that is essentially a duplicate of a big chunk of the Commit Move button code. We literally check for either player being a computer at this point, because the current player will change. Then we do a lot, including re-running startGame():

 while !playerWon {
		//check for computer player. Since there's no real difference in the code other than the test,
		//we can collapse this into a single thing.
		//right now the else is there as a test statement, will be removed
		if ((currentPlayer == "Blue") && (bluePlayerType == 2)) || ((currentPlayer == "Red") && (redPlayerType == 2)) {

		//make the computer move
		let theStartGameTuple = startGame(myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)
		buttonTitle = theStartGameTuple.myButtonTitle
		lastButtonClickedIndex = theStartGameTuple.myButtonToClick

		//here's where we duplicate a lot of code, but we only do it once, so it's fine.
		//we can look at fixing it in the next sprint maybe.
		let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

		arrayUsedButtonsList = theCommitTuple.myUnusedButtonArray
		arrayUsedMemberCountdown = theCommitTuple.myCountDownInt
		SOSFlag = theCommitTuple.mySOSFlag
		SOSCounter = theCommitTuple.mySOSCounter
		buttonBlank = true
		disableStartGameButton = true

		if (gameType == 2) && (SOSFlag) {
				var incrementScoreTuple = incrementScore(myCurrentPlayer: currentPlayer, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore, mySOSCounter: SOSCounter)
				redPlayerScore = incrementScoreTuple.myRedPlayerScore
				bluePlayerScore = incrementScoreTuple.myBluePlayerScore
		}

		var gameOverTuple  = isGameOver(myArrayUsedMemberCountdown: arrayUsedMemberCountdown, myGameType: gameType, myGridArray: theGame, mySOSFlag: SOSFlag, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore)

		gameWasDraw = gameOverTuple.myGameIsDraw
		generalGameWinner = gameOverTuple.myGeneralGameWinner
		playerWon = gameOverTuple.myGameIsOver

		if playerWon {
				return
		}

		if !playerWon {
				currentPlayer = changePlayer(myCurrentPlayer: currentPlayer)
		}

	} else if ((currentPlayer == "Blue") && (bluePlayerType == 1)) || ((currentPlayer == "Red") && (redPlayerType == 1))  {
			//print("Next player is \(currentPlayer) and is a human player")
	}
}

A lot of this is obvious, but some of it isn’t. For example, the real point of playerWon is to pop an alert that’s tied to the Commit Move button section (we’ll get there, calm down). incrementScore() looks like:

func incrementScore(myCurrentPlayer: String, myRedPlayerScore: Int, myBluePlayerScore: Int, mySOSCounter: Int) -> (myRedPlayerScore: Int, myBluePlayerScore: Int){
	var bluePlayerScore = myBluePlayerScore
	var redPlayerScore = myRedPlayerScore

	if myCurrentPlayer == "Blue" {
		//incrementing by number of SOS's created, not just by one no matter what
		bluePlayerScore = bluePlayerScore + mySOSCounter
	} else {
		redPlayerScore = redPlayerScore + mySOSCounter
	}

	let playerScoreTuple = (myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore)
	return playerScoreTuple
}

This only gets called in a general game, where you have a score, and the first SOS doesn’t win. We do the counter thing (which we’ll see in Commit Move) because in a general game, one move can create up to 8 SOS’s (I tested). So it gets called, then returns all the scores that have been incremented. As a matter of practice, only one is incremented, but doing it this way is easier and lets me use one function with one set of calling parameters and one return tuple, which makes it easier on me and no penalty on play. The “easier on me” is really important here. I really love that I can return tuples, that has SAVED my sanity

Then we call isGameOver():

func isGameOver(myArrayUsedMemberCountdown: Int, myGameType: Int, myGridArray: Game, mySOSFlag: Bool, myRedPlayerScore: Int, myBluePlayerScore: Int) -> (myGameIsOver: Bool, myGameIsDraw: Bool, myGeneralGameWinner: String) {

	var gameIsOver: Bool = false
	var gameIsDraw: Bool = false
	//we'll need this lateer
	var gridCountdown = myArrayUsedMemberCountdown
	var gameWinner: String = ""

	//check to see if gridCountDown is zero. If it is, game is over no matter what
	if gridCountdown > 0 {
		//grid countdown is not zero, check for other things
		if myGameType == 1 {
			//simple game
			if mySOSFlag {
				//there's an SOS, game is over
				//grid countdown to zero
				//gridCountdown = 0
				//disable all the buttons
				for i in 0..<myGridArray.gridCellArr.count {
					myGridArray.gridCellArr[i].buttonDisabled = true
				}
				//set the game over flag
				gameIsOver = true
				gameIsDraw = false
			} else {
				//mySOSFlag is not true, game is not over
				gameIsOver = false
				gameIsDraw = false
			}
		//we may not have to do anything here for a general game, since the SOS flag being true and game over being false runs the
		//score increment outside of this function
		}
	} else if gridCountdown <= 0 {
		//0 or less the game is over regardless of winner or not or game type
		//disable the game board
		//also, you CAN'T win a general game until all the squares are filled
		for i in 0..<myGridArray.gridCellArr.count {
			myGridArray.gridCellArr[i].buttonDisabled = true
		}
		//simple game draw
		if myGameType == 1 {
			if mySOSFlag {
				//there's a winner, so not a draw
				gameIsOver = true
				gameIsDraw = false
			} else {
				//no winner, is a draw
				gameIsOver = true
				gameIsDraw = true
			}
		} else {
			//general game all squares are filled is different
			//regardless of who won, game is over
			gameIsOver = true
			//red won
			if myRedPlayerScore > myBluePlayerScore {
				gameWinner = "Red"
				gameIsDraw = false
				//blue won
			} else if myBluePlayerScore > myRedPlayerScore {
				gameWinner = "Blue"
				gameIsDraw = false
				//Draw/no one won
			} else {
				gameWinner = "Draw"
				gameIsDraw = true
			}
		}
	}
	let gameIsOverTuple = (myGameIsOver: gameIsOver, myGameIsDraw: gameIsDraw, myGeneralGameWinner: gameWinner)
	return gameIsOverTuple
} 

If playerWon is true, the alert pops, we’re done. If it’s not, keep going and run that while loop again. Also, if the game isn’t over, we need to change the player, aka changePlayer():

func changePlayer(myCurrentPlayer: String) -> String {
	var newPlayer: String = ""
	if myCurrentPlayer == "Blue" {
		newPlayer = "Red"
	} else if myCurrentPlayer == "Red" {
		newPlayer = "Blue"
	}
	return newPlayer
}

This is simple enough that I hope I don’t have to explain it. Because I am not going to. Finally, there’s the label, the accessibilityLabel, the disabled status and me being paranoid about game button states:

 label: {
	Text("Start Game")
}
.onAppear(perform: {
//enable the start game button if it is visible
disableStartGameButton = false
})
.disabled(disableStartGameButton)
.accessibilityLabel("startButton")

Okay, we should be good to go for Commit Move. Maybe. As Skeletor would say: UNTIL WE MEET AGAIN!

Commit Move

Oh god, this will be a long section, Commit Move is one of the core parts of the game. Not that anything so far has been short, but still…

So in a manual game, that is with at least one human player, to finish making a move, you have to click the “Commit Move” button. This solves a few issues, like how to know when someone has actually made the move they want. Requiring an explicit action was the easy way. For some values of easy that is. Honestly, while there is a lot of code tied to “commit move” the basic functionality is pretty easy.

Having an explicit “commit move” button also lets us avoid a lot of problems with this one line, the first in the button action code:

gamePlayerTypeDisabled = true

Once you click “Commit Move”, the first thing it does is disable the game type and player type radio buttons, so you can’t change game or player type mid-move. The only way to re-enable them is the “new game” button or changing the grid size.

Next we have the functionality for recording a move in a manual game. This involves some controls that didn’t exist when I first started this post (because they were a stretch goal) but I’m so far ahead on this project, may as well include them) The record button is in ContentView:

 HStack(alignment: .top) {
			//record game button
			Toggle("Record Game:", isOn: $recordGame)
				.toggleStyle(.switch)
				.modifier(basicTextModifierNoFrame())
				.padding(.trailing, 10.0)
				/*.onChange(of: recordGame) {
					print("Toggle State is: \(recordGame)")
				}*/
			//playback button
			Button {
				//do stuff
			} label: {
				Text("Playback")
			}
			.disabled(playbackDisabled)
}

It’s a basic toggle button that when turned on, sets recordGame to true. The playback button is disabled by default unless the record button is enabled, and a game has finished. Then it’s enabled if you want to replay the game. The record button is set to Off/False on application launch, pressing the New Game button, or changing the grid size:

 self.recordGame = false
playbackDisabled = true

Back to the “Commit Move” button action code. First it tests for recordGame being true. Then if it is, it calls the addRecordedMove function, passing it the gameMoveRecord struct array, the index of the button that was clicked and is being committed via commit move, the array of game buttons, and the name of the current player. The gameMoveRecord struct array is an array of moveRecord structs that each hold important data about a game move, namely the button index, the title of the button (S or O), the name of the player, and the color of the button:

struct moveRecord {
	var moveIndex: Int
	var moveTitle: String
	var movePlayer: String
	var moveColor: Color
}

The array itself is an @State var:

@State var gameMoveRecord = [moveRecord]()

And it keeps a record of the moves made in a recorded game. So when Commit Move is clicked, and recording is enabled, we run this:

if recordGame {
			gameMoveRecord = addRecordedMove(myGameRecord: gameMoveRecord, myCommittedButtonIndex: lastButtonClickedIndex, myGridArray: theGame, myCurrentPlayer: currentPlayer)
}

which calls, as we said, addRecordedMove():

func addRecordedMove(myGameRecord: [moveRecord], myCommittedButtonIndex: Int, myGridArray: Game, myCurrentPlayer: String) -> [moveRecord] {
	var tempGameRecord = myGameRecord
	let theMove = myGridArray.gridCellArr[myCommittedButtonIndex]
	let theTempMoveRecord = moveRecord(moveIndex: theMove.index, moveTitle: theMove.title, movePlayer: myCurrentPlayer, moveColor: theMove.backCol)
	tempGameRecord.append(theTempMoveRecord)
	return tempGameRecord
} 

That’s pretty straightforward: it creates a temp mutable version of the moveRecord array, a temp version of the commited move index (mostly to make the grid array reference easier to read.) It creates an instance of the moveRecord struct with all the current move data, and appends that onto theTempGameRecord which it then returns back to ContentView

Next, the Commit Move button action calls commitMove the function:

commitMove()

let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

commitMove() in and of itself isn’t that long, but it does a lot, both the function parameters are pretty long:

func commitMove(myCommittedButtonIndex: Int, myUnusedButtons: [Int],myGridArray: Game, myCurrentPlayer: String, myArrayUsedMemberCountdown: Int) -> (myUnusedButtonArray: [Int], myCountDownInt: Int, mySOSFlag: Bool, mySOSCounter: Int) {

First, we create some mutable copies of the vars passed to the function:

var theTempArray = myUnusedButtons
var theTempCounter = myArrayUsedMemberCountdown

checkForSOS()

(Going to put this into sections for Commit Move, because this is SO long)

Next, we want to check for an “SOS” because if it’s a simple game, an SOS ends it, and if not, then we need to know to increment the score. So we call checkForSOS(), which does do a lot, because it’s kind of complicated. Remember, in a general game, you can have one move create multiple SOS’s, up to 8 in fact, (I checked) and there’s different patterns. An S can create an SOS in a spoke pattern from it. Two vertical directions, two horizontal, and four diagonal, since an S is always at the end of an SOS. So we have to check for all 8. An O, being in the middle, really only has four: one vertical, one horizontal, two diagonal.

So, depending on the letter placed, we can have anywhere from one to 8 SOS’s happening. Which means we need 8 checks for S and four for O. I decided, in the interest of just having the function be long, but simple, to check for all possibilities. It takes zero time on a human scale, and that allows it to be really straightforward, just a series of if statements all of which are checked depending on letter, eight for S, four for O. First, the call to checkForSOS()

let theSOSTuple = checkForSOS(myGridArray: myGridArray, myLastButtonClickedIndex: myCommittedButtonIndex, myGridSize: myGridArray.gridSize, myCurrentPlayer: myCurrentPlayer)

Here’s the actual def. for checkForSOS:

func checkForSOS(myGridArray: Game, myLastButtonClickedIndex: Int, myGridSize: Int, myCurrentPlayer: String) -> (mySOSFlag: Bool, mySOSCounter: Int)

Because we can have multiple SOS’s in a move, we have to count those:

var SOSCounter: Int = 0

Now, because of how letters can be placed on an edge button, we need to make sure we treat those differently than one placed dead in the middle. What you’ll notice is that I only check for right and bottom edges, and I have a leftmost and rightmost flag. The right and bottom edges are the only ones we have to calculate. The other two are either the x-coord being 0 (leftmost edge) or the y-coord is 0, (topmost edge), so we don’t have to calculate those values:

let buttonRightEdgeCheck = myGridSize - 1
let buttonBottomEdgeCheck = myGridSize - 1
var buttonLeftmostFlag: Bool = false
var buttonRightmostFlag: Bool = false

Next, we need a flag for the existence of an SOS:

var SOSFlag: Bool = false

Now, a switch statement to do the initial flag sets:

switch myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord {
		case 0:
			buttonLeftmostFlag = true
		case buttonRightEdgeCheck:
			buttonRightmostFlag = true
		default:
			buttonLeftmostFlag = false
			buttonRightmostFlag = false
}

Now we set up some other vars like the current title (I could directly refer to it, but it’s tedious do do that, and makes the code a bit hard to read) and the distance from the right and bottom edges of the button:

let theCurrentButtonTitle = myGridArray.gridCellArr[myLastButtonClickedIndex].title

let distanceFromRight = buttonRightEdgeCheck - myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord

let distanceFromBottom = buttonBottomEdgeCheck - myGridArray.gridCellArr[myLastButtonClickedIndex].yCoord

Again, the top and left edges are zero for one of the coordinates, those are easy to check inline. So we do.

First we do the S checks. To make things a bit easier for me (important!), I group the S checks. First we do the LTR (Left-To-Right) checks. Because of this, we need to make sure the buttonRightMost flag is false, (if it’s true, the button is on the far right edge, LTR is not valid), and then for LTR, we want to make sure the button is at least three buttons from the edge, since LTR, SOS is three buttons. Less than three, again, invalid. If both of those are true, then an SOS is possible, so we go ahead and check:

if theCurrentButtonTitle == "S" {
		if !buttonRightmostFlag {
			if distanceFromRight >= 2 {
				let nextCellIndex = myLastButtonClickedIndex + 1
				let secondCellIndex = myLastButtonClickedIndex + 2
				if (myGridArray.gridCellArr[nextCellIndex].title == "O") && (myGridArray.gridCellArr[secondCellIndex].title == "S") {
					setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: nextCellIndex, myThirdIndex: secondCellIndex, myGridArray: myGridArray)
					SOSFlag = true
					SOSCounter += 1
				}
				if distanceFromBottom >= 2 {
					let nextCellIndex = (myLastButtonClickedIndex) + (myGridSize + 1)
					let secondCellIndex = (nextCellIndex) + (myGridSize + 1)
					if (myGridArray.gridCellArr[nextCellIndex].title == "O") && (myGridArray.gridCellArr[secondCellIndex].title == "S") {
						setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: nextCellIndex, myThirdIndex: secondCellIndex, myGridArray: myGridArray)
						SOSFlag = true
						SOSCounter += 1
					}
				}

Of COURSE there’s another function in the function we called from a function. In this case, if there’s an SOS, we want to set the color of all three buttons to that of the player that made the call. I thought about a gradient for cases where it was already a color, but that was a really weirdly large amount of work to implement, so I didn’t. our next function is setSOSButtonColor()

setSOSButtonColor()

This does what it seems, sets the color for all three buttons in the SOS. Here’s the function definition:

func setSOSButtonColor(myCurrentPlayer: String, myFirstIndex: Int, mySecondIndex: Int, myThirdIndex: Int, myGridArray: Game)

And here’s the actual code:

func setSOSButtonColor(myCurrentPlayer: String, myFirstIndex: Int, mySecondIndex: Int, myThirdIndex: Int, myGridArray: Game) {
	if myCurrentPlayer == "Blue" {
		myGridArray.gridCellArr[myFirstIndex].backCol = .blue
		myGridArray.gridCellArr[mySecondIndex].backCol = .blue
		myGridArray.gridCellArr[myThirdIndex].backCol = .blue
	} else if myCurrentPlayer == "Red" {
		myGridArray.gridCellArr[myFirstIndex].backCol = .red
		myGridArray.gridCellArr[mySecondIndex].backCol = .red
		myGridArray.gridCellArr[myThirdIndex].backCol = .red
	}

}

It’s pretty simple: we pass it the three buttons we care about and the Game grid. If the current player is Blue, we set the background colors of the buttons to blue. If Red, red. Since Game is a class and is pass by reference, we don’t need a return, so it’s a void function. Now, back to checkForSOS()!

checkForSOS() cont’d

All the S checks work about the same. First, we make sure we can validly check for the SOS we want. Then because SOS is three buttons, we set up the second and third buttons (nextCellIndex and secondCellIndex.) Initially, I thought this was going to be a lot of tedious xCoord and yCoord comparisons, but I realized as I was working it out that I didn’t need any of those, just the indexes. For example, if I’m checking for a LTR horizontal, and the first button has an index of n, then the second button’s index is n + 1 and the third is n + 2. The rest were similarly simple:

 LTR Diag Down is (index) + (gridsize + 1)
 LTR Diag Up is (index) – (gridsize -1)
Vertical Up is index – gridsize
Vertical Down is index + gridsize
RTL Horizontal is index – 1
RTL Diag Down is (index) + (gridsize – 1)
RTL Diag Up is (index) – (gridsize + 1)

That is far more simple to implement than some “if x is increasing and y is decreasing” crap. Then if nextCellIndex is an O and secondCellIndex is an S, we have an SOS, call setSOSButtonColor(), set SOSFlag to true and increment SOSCounter.

I did this all as a series of IF statements because while i could have had yet another function (YAF), then I’d have a fairly complex function call, and within that function, the only “repetition” i’d save would be in calling setSOSButtonColor() and setting the SOSFlag to true and incrementing the SOSCounter. So 36 or so lines of code, but now I also am slinging function calls back and forth, which is resource overhead, and it makes tracking what’s happening if something goes wrong more tedious.

More importantly, it really doesn’t make the code significantly more readable or easier to debug, it doesn’t radically improve the speed of the function execution, and my setup code would be somewhat longer anyway…in other words, I didn’t see how doing that didn’t make anything better enough to justify the work, and it didn’t solve any actual problem.

So the S if chain does its thing, and then we get to the O if chain, which is a little different. Because O is in the middle, we only need four checks as there are only four unique combinations for an O since it is always in the middle, and we have to check on either side of the O. So the basic functionality is the same, we just tweak some things. For example, while an S has to have two squares between it and an edge, an O only needs one. An S in the corner can have three possible SOS’s, an O in the corner can have none. For S, we only care about boundary checking in one direction, for O we have to check in two. That kind of thing:

if theCurrentButtonTitle == "O" {
		 if (myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord >= 1) && (distanceFromRight >= 1) {
			let rightAdjacentCellIndex = myLastButtonClickedIndex + 1
			let leftAdjacentCellInded = myLastButtonClickedIndex - 1
			if (myGridArray.gridCellArr[rightAdjacentCellIndex].title == "S") && (myGridArray.gridCellArr[leftAdjacentCellInded].title == "S") {
				setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: rightAdjacentCellIndex, myThirdIndex: leftAdjacentCellInded, myGridArray: myGridArray)
				SOSFlag = true
				SOSCounter += 1
		}
}

It’s similar to an S check. We set the index for the buttons on either side of the O, and if both are an S, then setSOSButton(), SOSFlag = true, SOSCounter increments. Once all the checks are done, we pack SOSFlag and SOSCounter into a tuple and return them to commitMove():

let myCheckForSOSTuple = (mySOSFlag: SOSFlag, mySOSCounter: SOSCounter)
return myCheckForSOSTuple

commitMove() cont’d