Thursday, May 8, 2014

A Chess Project, Part 13 (The Final Chapter)

Intro

I can see the light at the end of the tunnel! It's an alternating black and white checkered light, but still, a light nonetheless. How many of you thought it would take 13 blog posts to get here? Well I sure didn't, but I'm glad we're almost done. Here in week 13 we'll be modifying our Web API that we created months ago. We'll make it call into our spiffy chess AI dll in order to determine the best move from a given board position. After all, that's what we were trying to do from the very start!

 

 Code Changes

First, here is the absolute latest code as it stands right now, before today's modifications. It's not the most optimized, and I dare say the AI isn't really all that great, but hey the point of this blog was learning new technologies not to make the perfect chess AI.

Let's get to work. Open up BlogChessController from the BlogChessApi project. That method named PostBestMove is the one we want to do our modifications in. Here's what it looks like currently:

        public HttpResponseMessage PostBestMove(ChessGame game)
        {
            try
            {
                //validate the game
                var validator = new ChessGameValidator(game);
                if (!validator.Validate())
                    return Request.CreateResponse(HttpStatusCode.BadRequest, validator.ValidationIssues);

                //calculate the best move
                IChessValidMoveCalculator moveCalculator = new ChessValidMoveCalculator(game);
                var validMoves = moveCalculator.CalculateValidMoves();

                //return the best move wrapped in an http "ok" result
                return Request.CreateResponse(HttpStatusCode.OK, validMoves);
            }
            catch (Exception ex)
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
            }
        }


You'll notice we don't yet have a call into the AI dll. At the time we originally made this method we had no such AI dll, so I can forgive us. Before we can do the code though, we need to reference BlogChess.AI.Basic1 from the Web API project. Go ahead and do that now (I'm assuming you've seen that enough that no screenshots are necessary). Add a using statement up at the top of the unit while you're at it.

All we really need to do is add in a call to our Negamax evaluation, then add in a little bit of logic to look for win/draw conditions. All of this is just calling methods we've already written. Your newly modified should look like the following:

        public HttpResponseMessage PostBestMove(ChessGame game)
        {
            try
            {
                //validate the game
                var validator = new ChessGameValidator(game);
                if (!validator.Validate())
                    return Request.CreateResponse(HttpStatusCode.BadRequest, validator.ValidationIssues);

                //calculate the best move
                var bestMove = Negamax.Evaluate(game, 2, game.GameStatus == GameStatus.WhitesTurn);
                var responseGame = bestMove.Item2;
                responseGame.CurrentEvaluation = bestMove.Item1;

                //is a win or draw, set game move accordingly
                IChessValidMoveCalculator moveCalculator = new ChessValidMoveCalculator(game);
                var availableMoves = moveCalculator.CalculateValidMoves();
                if (availableMoves.Count == 0)
                {
                    var isKingInCheck = moveCalculator.IsKingInCheck(game.GameStatus == GameStatus.WhitesTurn, ((IList<ChessBoard>)game.Positions)[0]);
                    //look for checkmate
                    if (availableMoves.Count == 0 && isKingInCheck)
                        responseGame.GameStatus = game.GameStatus == GameStatus.WhitesTurn ? GameStatus.WhiteWin : GameStatus.BlackWin;
                    //look for draw
                    else if (availableMoves.Count == 0 && !isKingInCheck)
                        responseGame.GameStatus = GameStatus.Draw;
                }
                else //just the next turn now
                {
                    responseGame.GameStatus = game.GameStatus == GameStatus.BlacksTurn ? GameStatus.WhitesTurn : GameStatus.BlacksTurn;
                }

                //return the best move wrapped in an http "ok" result
                return Request.CreateResponse(HttpStatusCode.OK, responseGame);
            }
            catch (Exception ex)
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
            }
        }



As advertised, just a couple minor additions to call our evaluator and check for end of game.


Testing It

Remember way back when, that web forms project BlogChessApiFlexer? It's right there in the solution, and it's just itching to call the modified web api method. We had already coded a call into the web api (in default.aspx.cs), but prior to now we were just passing in a dummy request and expecting a dummy response. Well now it's time for an appropriate request and response!

        public ChessGame Game
        {
            get
            {
                return Session["Game"] as ChessGame;
            }
            set
            {
                Session["Game"] = value;
            }
        }

        protected void btnServerTest_Click(object sender, EventArgs e)
        {
            //1. Create and setup client object
            using (var client = new HttpClient() { BaseAddress = new Uri("http://localhost:11482/"), Timeout = TimeSpan.FromSeconds(300) })
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                //2. Create a blank chess game to send up in the post request
                if (Game == null)
                {
                    Game = new ChessGame() { GameStatus = GameStatus.WhitesTurn };
                    var positions = new List<ChessBoard>();
                    var sampleBoard = new ChessBoard(true);
                    sampleBoard.Board = new short[8, 8] { { -4, -2, -3, -5, -6, -3, -2, -4 }, { -1, -1, -1, -1, -1, -1, -1, -1 }, { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0 }, { 1, 1, 1, 1, 1, 1, 1, 1 }, { 4, 2, 3, 5, 6, 3, 2, 4 } };
                    positions.Add(sampleBoard);
                    Game.Positions = positions;
                }
                int numPositions = ((IList<ChessBoard>)Game.Positions).Count;

                //3. Send the request
                var response = client.PostAsJsonAsync("api/BlogChess/BestMove", Game).Result;
                if (response.IsSuccessStatusCode)
                {
                    //4. Read the result from the response, display the "best move"
                    var result = response.Content.ReadAsStringAsync().Result;
                    var resultObject = JsonConvert.DeserializeObject<ChessGame>(result);
                    Game.GameStatus = resultObject.GameStatus;
                    ((IList<ChessBoard>)Game.Positions).Clear();
                    for (int positionIndex = 0; positionIndex < numPositions + 1; positionIndex++)
                    {
                        if (((IList<ChessBoard>)resultObject.Positions).Count > positionIndex)
                            ((IList<ChessBoard>)Game.Positions).Add(((IList<ChessBoard>)resultObject.Positions)[positionIndex]);
                    }
                    lblResult.Text = "Success! :" + result;
                }
                else //5. Request failed; tell the user what happened
                    lblResult.Text = "Failzor'd!: " + response.StatusCode.ToString() + "::" + response.ReasonPhrase;
            }
        }



Let's start with the easiest part, the propert Game of type ChessGame. If you've done webforms before, this shouldn't require much of an explanation. This is just a handy, type-safe way for me to be able to reference the current game object from the session so I can persist information about a single game in memory. Moving on to the button click event...

And here's the meat. Some of this was already here. As I said above, we were already calling the web api. What's new is we're now creating and serializing a chess game object (the one from our session), and we're passing this game object to the web api. Then we're taking the result of this call and updating the Game property. This means that, if you keep clicking that button, the AI will play itself! How cool is that? Well it's moderately cool, as it's incredibly slow. But hey, baby steps.

Wrapping it Up

Thanks for sticking with this series of articles everybody. As can happen with coding endeavors, it got away from me a bit. I thought this might be a 4 or 5 article series, not 13. I hope you learned a few new things, and maybe even gained a little bit of interest in chess in the process of reading these.

Oh and here's the absolutely final code, all fanciful and whatnot.

What's Next?

There are a few things you could do really. You could load AI dlls dynamically, configurable via database entry or config file. This would make it easier for other people to create AI dlls that plug into your web api, and you could have clients configured to choose a specific AI. You could also optimize the existing AI dll further (or just make your own) to make it much much quicker (hint: bitboard!). Or just go have a beer, or martini, or diet dr pepper. Whatever.


Resources

online chess board editor at Apronus.com
Chess.com, a great site for everything chess related 

No comments:

Post a Comment