Using GKMinmaxStrategist

I wear many hats. One of those hats is writing games. You can find the games I’ve written as Cetuscript Systems or WeGame Corp.

Hexin is a space chess game and I wanted a reasonable computer opponent for the players. I written a chess/checkers like game previously (not currently available) and my quick and dirty AI was okay-ish but not very challenging. For Hexin, I wanted something better.

With Hexin too, I wanted to explore the new game frameworks from Apple. GameplayKit is one of those new frameworks and it supplies a number of strategists classes. The GKMinmaxStrategist looked like it fit my requirements.

Tweaking the model for GKMinmaxStrategist was interesting. For my chess-like game, it boiled down to figuring out how to calculate a score for a particular board.

At the beginning, I tried working out a score by the value of pieces in the game. Hexin has three areas of the game: hanger, deck, and board. Pieces move from the hanger to the deck to in play on the board. Where the piece is determines if it is in play or how soon it might be to be put in play. Pieces in the hanger are not as valuable as pieces on the board.

A simple value of a player’s current pieces worked but not very well. The Strategist would know to improve the value of the player’s position but it didn’t recognize disaster to the plan. The Strategist would send its pieces on suicide missions.

To adjust for this, I had to also account for the move after. I was a bit worried about this because I didn’t want to spend the time writing an AI for the game. The Strategist should give me a reasonable opponent without going Deep Blue.

But, in the end, it seemed that accounting for the move after was enough to give me a reasonable opponent.

func score(for player: GKGameModelPlayer) -> Int {
    guard let who = player as? ModelPlayer else { return GKGameModelMinScore }
    
    let disaster    = GKGameModelMaxScore * 2
    let foolish     = 0
    
    let friend = who.side
    let enemy = friend.opponent
    
    let friendReach = self.evaluateReach(side: friend)
    let enemyReach = self.evaluateReach(side: enemy)
    
    var friendTotal:Int = self.evaluateValue(friend: friend) - self.whosePlaying[friend.rawValue].value
    var enemyTotal:Int  = self.evaluateValue(friend: enemy) - self.whosePlaying[enemy.rawValue].value
    
    var friendScore:Int = 0
    var enemyScore:Int  = 0

The friendReach and enemyReach are the next moves. Based on the current game, what can a player reach.

The friendTotal and enemyTotal are initialized with the difference between the initial value for the player and the current configuration value for the player. This gives the sense of are we better or worse off than when we started to figure out our next move.

    for (coord,_) in enemyReach {
        if let unit = self.board.units[coord], unit.side == friend {
            if unit.flavour == .Station {
                enemyScore += disaster  // this is a bad move
            } else if unit.flavour == .Portal && self.clusters[friend.rawValue].station.inHanger {
                enemyScore += disaster  // this is a bad move
            } else {
                enemyScore += unit.flavour.value
            }
        }
    }

I then go through the enemy’s reach. In the game, Stations and Portals are critical to play. The object of the game is to take the opponent’s Station or prevent the opponent from getting their Station onto the board by taking their Portal. If the enemy can take your Station, it is a disaster.

    for (coord,result) in friendReach {
        if let unit = self.board.units[coord], unit.side == enemy {
            if let _ = enemyReach[coord] {
                // this is a location that the enemy can take. not the best move.
                friendScore -= foolish
            } else {
                if unit.flavour == .Station {
                    friendScore += GKGameModelMaxScore
                } else if unit.flavour == .Portal && self.clusters[enemy.rawValue].station.inHanger {
                    friendScore += GKGameModelMaxScore
                } else {
                    if result == .Power {
                        friendScore += ValueForFighterPowered
                    } else {
                        friendScore += unit.flavour.value
                    }
                }
            }
        }
    }

Foolish is putting your piece into harms way. But I found that giving foolish a non-zero value didn’t help much.

Taking the opponent’s Station or Portal wasn’t weighted the same as the disaster of losing your own. Over weighting the victory lead to suicide missions. Also, in the game, there are power-ups. Using a power-up needed to be accounted for.

    friendTotal += (friendScore * TakePowerValueFriendFactor)
    enemyTotal += (enemyScore * TakePowerValueEnemyFactor)

The TakePowerValueFriendFactor is slightly larger than TakePowerValueEnemyFactor. The friendScore is given a bit more weight than the enemyScore.

    let diff    = friendTotal - enemyTotal
    var value   = max( min( GKGameModelMaxScore - 1000, diff), GKGameModelMinScore)

There is a gap of 1000 in the pinning of the score. This allows for the move that wins the game to be detected.

    if self.isLossForSide(side: who.side) {
        value = GKGameModelMinScore
    }
    
    if self.isLossForSide(side: who.side.opponent) {
        value = GKGameModelMaxScore
    }
    
    return value
}

My goal in using the GKMinmaxStrategist was to present a computer opponent that wouldn’t be too easy to beat, that would give enough practice for a player so they could learn the game. The model that I’ve tweaked to, I think, does that.

Hexin is available for macOS and iOS. And it is free 🙂

APNs Auth Key with HTTP/2 and PHP

I am not new to Apple Push Notification Service so when it came time to revamp a projects APNs setup I first cringed then took a deep breath and then went to the start of the mountain mule trail that is the process of getting this task done.

The trail is full of arcane cryptography spells and linux rituals. But it all starts with visiting the Apple developer site and getting two certificates: one for the sandbox and one for production. As I went to the Apple developer site, I knew that the next steps would be to flip through the spell books stored in Google for the spells to turn what I got from Apple into what I could use in PHP.

I started with the sandbox certificate and it was the familiar process of requesting a request from my local Keychain assistant then presenting that request to the Apple developer site to get the little scrape of text that would become a pem with the appropriate crypto-rituals. This is a yearly ritual both tedious and familiar.

Next I went to get the production certificate. But instead of being sent to my local Keychain assistant, I could only pick up the magic APNs Auth Key that promised to never expire. And the key would open both the door to the sandbox server and the door to production server. Also, once downloaded, the key would not be presented again. Ever. A magic key that could open both doors and never expire sounded good. “But what’s the catch?” came the quiet voice inside my head.

The catch was that my servers in this project can’t use the key.

The mountain that this mule trail winds its way through has a complex topology with many valleys and passes. I am sure there are easier passes out there, but to find these passes you’d have to start on different trails. My trail starts with PHP. Perhaps if my trail started with Node.js or Ruby or something else, the pass across the mountain could have been found easier. But that is not relevant.

With the APNs Auth Key in hand, the first step is to try to figure out if there is a map to this trail. Flipping through Google, you’ll find the APNs Provider API on the Apple developer site. The API uses HTTP/2 network protocol with JSON Web Tokens (JWT) for authentication.

The easy part of the trail was the creation of JSON Web Tokens. There are a number of PHP packages that will handle this for you.

The first catch is that Apple’s APNs servers are using HTTP/2. So that means that I rebuild curl for HTTP/2.

Second catch is that they need openssl 1.0.2. I don’t want to mess around too much with recompiling a lot of things so I look at a couple flavours of Linux that Goole cloud will set up for me. Both Debian and Centos won’t play nice with me.

But there is Ubuntu 16.04 with openssl 1.0.2 out of the box!

So all I need to do is set up another server to handle the APNs stuff. But…

Of course at the start of this mountain trail, I came across this very helpful tutorial: Send Push Notifications to iOS Devices using Xcode 8 and Swift 3. Of course, the title of the tutorial isn’t complete. They missed the really important part. They should have added “and Node.js v4.” My trek down this arduous trail was to avoid adding Node.js to one of my other existing servers.

At some point though, the best path is to ditch the trail. I have stuff that isn’t broken, doesn’t need to get fixed (yet) on an existing server. If my map to my mountainous trail involves setting up another server, I don’t need to stick with PHP.

So. I fired up the Ubuntu 16.04, installed Node.js, and installed the various packages to support the apn.js script. All very easy.

The next step was to get what needed to be posted. For that, I opened an endpoint on my api on the primary server to query for stuff that needed to be posted. Done and working just fine.

As a side benefit, I’m using this new server to handle various other timed functions with Node.js. That functionality is no longer sitting on the primary server.

 

In the end, I must apologize. The title of this blog posting is “APNs Auth Key with HTTP/2 and PHP” but I don’t tell you how to do that. Instead, my advice is to find or configure a server that handles HTTP/2 and openssl 1.0.2, and then install Node.js v4 to send your notifications to Apple’s server.