Difference between revisions of "Blackjack"
(24 intermediate revisions by the same user not shown) | |||
Line 3: | Line 3: | ||
This tutorial will be the guide for an intro to Web Development project building Blackjack to gain a basic understanding of the major building blocks of web applications: APIs, MySQL databases, Client Webpages, Cloud Servers and Github. | This tutorial will be the guide for an intro to Web Development project building Blackjack to gain a basic understanding of the major building blocks of web applications: APIs, MySQL databases, Client Webpages, Cloud Servers and Github. | ||
− | / | + | [[File:PlatformOverview.png |500px]] <br/> |
− | / | + | Here's what the final project will look like (it's terrible, but sufficient for our needs): |
+ | |||
+ | [[File:Blackjackui.png |500px ]] <br/> | ||
Each project will utilize the following major pieces: | Each project will utilize the following major pieces: | ||
Line 45: | Line 47: | ||
== Materials/Prerequisites == | == Materials/Prerequisites == | ||
− | + | As always, you will be using Git. It is highly reccommended you use Git in the command line. There are really only five Git commands that matter: | |
− | + | ||
+ | <source lang="bash"> | ||
+ | |||
+ | # Copy git repository to computer | ||
+ | git clone <repository URL> | ||
+ | |||
+ | # see current repo data like which files have been modified since the last commit | ||
+ | git status | ||
+ | |||
+ | # add all files to the next commit | ||
+ | git add . | ||
+ | |||
+ | # join files together in one package | ||
+ | git commit -m "COMMIT_MESSAGE" | ||
+ | |||
+ | # send changes up to the server | ||
+ | git push origin master | ||
− | / | + | </source> |
We will be utilizing a variety of tools/technologies to accomplish this project. | We will be utilizing a variety of tools/technologies to accomplish this project. | ||
Line 59: | Line 77: | ||
If you are using NodeJs, we expect you to have installed a version of Node, which can be obtained [https://nodejs.org/en/ here]. | If you are using NodeJs, we expect you to have installed a version of Node, which can be obtained [https://nodejs.org/en/ here]. | ||
− | At this point you should | + | Find a Git repository that you can use. Feel free to use the one already established for your group, just locate everything in a subfolder (i.e. GroupRepo > Blackjack). |
+ | |||
+ | Gain a basic familiarity with the API we'll be using, (particularly the schema of the responses), [http://deckofcardsapi.com/ deckofcardsapi] | ||
+ | |||
+ | At this point you should '''complete the MySQL database on AWS tutorial''', which can be found [[MySQL_database_on_AWS|here]]. | ||
== Process == | == Process == | ||
Line 106: | Line 128: | ||
</source> | </source> | ||
− | Your database is all good to go now! | + | Your database is all good to go now! After each game the table should populate, and may resemble the following: |
+ | |||
+ | [[File:Blackjacktable.png |500px]] <br/> | ||
=== Setting up our project directory === | === Setting up our project directory === | ||
− | In this step we'll set up our files/folders for the project. | + | In this step we'll set up our files/folders for the project. The main concern here is we want to isolate any files which should be accessible through the API's file server from our code files. This will help ensure the security of our source code. |
+ | |||
+ | Create the following directory structure: | ||
+ | |||
+ | <source lang="text"> | ||
+ | Repository Root | ||
+ | |-public | ||
+ | |- index.html | ||
+ | |- index.css | ||
+ | |- index.js | ||
+ | |- server.js/server.py | ||
+ | |- config.json | ||
+ | |||
+ | </source> | ||
+ | |||
+ | Any files we want our browser to be able to access, we will store exclusively in our '''public''' folder/subfolders, while our server code will remain outside that folder. | ||
+ | |||
+ | Our config.json is another best-practices thing we will do. In it we will store things like the root for our deckofcards API endpoint (to reduce the number of [https://softwareengineering.stackexchange.com/questions/365339/what-is-wrong-with-magic-strings magic strings]), as well as things we wish to keep secure (such as our MySQL connection information). In industry, we would probably tell git to ignore our config.json file and would manually upload it to the server, so that any secure information stored in it wouldn't be available to people who have visibility to your git repository, however that particular application is outside the scope of this tutorial. | ||
+ | |||
+ | We'll also go ahead and fill the config.json with the data we'll use to configure our application: | ||
+ | |||
+ | <source lang="json"> | ||
+ | { | ||
+ | "PORT": 4000, | ||
+ | "apiEndpoint": "https://deckofcardsapi.com/api/deck", | ||
+ | "mysqlHost": "YOUR_RDS_INSTANCE_HOST", | ||
+ | "mysqlUser": "YOUR_DB_USER", | ||
+ | "mysqlPassword": "YOUR_DB_PASSWORD", | ||
+ | "mysqlDatabase": "YOUR_DB_NAME" | ||
+ | } | ||
+ | </source> | ||
=== NodeJS API Server (Or skip to python if using python) === | === NodeJS API Server (Or skip to python if using python) === | ||
+ | |||
+ | This section will be dedicated to developing our blackjack client API. The purpose of this API is threefold: to interface between the client and requests for web resources (linked stylesheets/scripts, serving webpages and static assets), interfacing between the client and the AWS RDS instance, and interfacing between the client and the deckofcards API. | ||
+ | |||
+ | The first thing you'll need to do is establish your node project and install packages to assist in development. Open up a terminal in the root directory of your project and execute | ||
+ | |||
+ | <source lang="bash"> | ||
+ | npm init | ||
+ | </source> | ||
+ | |||
+ | Step through the prompts, feel free to leave them all as default. You should now see a '''package.json''' file has been created in your project's directory. Next, we will install all the packages we need to facilitate our API. This will consist of: | ||
+ | |||
+ | * [https://www.npmjs.com/package/axios axios], for making API calls from our server | ||
+ | * [https://www.npmjs.com/package/mysql mysql], for making MySQL queries against a database | ||
+ | * [https://www.npmjs.com/package/express express], for intercepting and handling requests to our server | ||
+ | * [https://www.npmjs.com/package/body-parser body-parser], for parsing POST request bodies (not used in this project, but you will want this 9/10 times in API development). | ||
+ | |||
+ | All four of these packages are well reputed, well maintained, and well documented, and you should have no trouble finding resolutions to any issues you have with them. In the console, type: | ||
+ | |||
+ | <source lang="bash"> | ||
+ | npm install --save axios mysql express body-parser | ||
+ | </source> | ||
+ | |||
+ | If you look in your '''package.json''', you should now see a section titles '''requirements''' which contains the four packages (and an associated minimum version number). You should also see a node_modules folder with hundred(s) of subfolders. | ||
+ | |||
+ | In the meantime, we will also want to ensure our start script is set correctly. Check the '''scripts''' section of your '''package.json''' to ensure the '''start'' entry is specified: | ||
+ | |||
+ | <source lang="json"> | ||
+ | "scripts": { | ||
+ | ... | ||
+ | "start": "node server.js", | ||
+ | ... | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | This will allow you to run your server.js file from the terminal using the command '''npm start'''. | ||
+ | |||
+ | The last thing you're going to want to do is ensure that you don't commit your '''package-lock.json''' or '''node_modules''' to git. If you haven't already, create a file called '''.gitignore''' and add the following entries: | ||
+ | |||
+ | <source lang="txt"> | ||
+ | **/node_modules/ | ||
+ | **/package-lock.json | ||
+ | </source> | ||
+ | |||
+ | Now that we have configured our node project, we will assemble our server.js code. | ||
+ | |||
+ | For the following sections, we will be diving into server-side javascript. If any of the syntax is unfamiliar to you, please check out the or look up any of the relevant syntax to help clarify what's going on. | ||
+ | |||
+ | Add the following lines to import the required packages to our server: | ||
+ | |||
+ | <source lang="js"> | ||
+ | // import appropriate modules | ||
+ | |||
+ | // mysql module, for mysql requests | ||
+ | const mysql = require('mysql'); | ||
+ | |||
+ | // util, to allow us to use async/await | ||
+ | // util is built in to nodejs, so we don't have to install it | ||
+ | const utils = require('util'); | ||
+ | |||
+ | // axios, to make API requests | ||
+ | const axios = require('axios'); | ||
+ | |||
+ | // express, to maintain our API server | ||
+ | const express = require('express'); | ||
+ | |||
+ | // API post body middleware | ||
+ | const bodyParser = require('body-parser'); | ||
+ | |||
+ | // local json file with important data/global constants | ||
+ | const config = require('./config.json'); | ||
+ | </source> | ||
+ | |||
+ | Notice that since our modules are packages, we simply specify the package name and node handles finding the appropriate code to import, but since our config file is something we've created, we must specify the full path to that file. | ||
+ | |||
+ | Next we'll finish setting up our server and database connection, as well as do a little trick to allow us to avoid using callbacks for all our MySQL calls. (if you don't know what I mean when I say callback, or are unfamiliar with the idea of asyncronous programming, check out [https://www.zeolearn.com/magazine/asynchronous-nature-of-javascript this introduction to the asyncronous nature of javascript]). | ||
+ | |||
+ | <source lang="js"> | ||
+ | // initialize our API server | ||
+ | const app = express(); | ||
+ | |||
+ | // POST body middleware (parses POST body to json for use in routes) | ||
+ | app.use(bodyParser.json()); | ||
+ | |||
+ | // establish mysql connection settings | ||
+ | const conn = mysql.createConnection({ | ||
+ | host: config.mysqlHost, | ||
+ | user: config.mysqlUser, | ||
+ | password: config.mysqlPassword, | ||
+ | database: config.mysqlDatabase | ||
+ | }); | ||
+ | |||
+ | // make conn.query a promise, this allows us to use 'await' instead of a callback | ||
+ | // don't worry about the details of this, just include this line | ||
+ | // now instead of using 'conn.query(query, params, callback)' we'll use 'await query' | ||
+ | const query = utils.promisify(conn.query).bind(conn); | ||
+ | |||
+ | // connect to our mysql database | ||
+ | conn.connect((err) => { | ||
+ | if (err) { | ||
+ | console.log('Unable to connect to mysql'); | ||
+ | throw err; | ||
+ | } | ||
+ | }); | ||
+ | </source> | ||
+ | |||
+ | Than's all the setup we need! Now we can dive into our API routes. | ||
+ | |||
+ | An API is just a collection of routes and functions used to handle those routes. Essentially, we tell our server that when someone makes a request to '''www.myurl.com/someEndpoint''' we want to execute function x, and when someone makes a request to '''www.myurl.com/anotherEndpoint''' we want to execute function y. | ||
+ | |||
+ | As you may recall, the first purpose of our API server is to send files to the client, so we will set up two routes for that purpose: the first is so that when someone navigates to our index url ('''www.myurl.com/''') we serve our '''index.html''' file, and the second is to serve all files in the public folder (this is mainly to support HTML link attributes). | ||
+ | |||
+ | <source lang="js"> | ||
+ | // match base route to index.html | ||
+ | // this is a basic express route. the general format is app.get(routeString, callback) | ||
+ | // where 'get' is the HTTP method (i.e. GET/POST/PUT), | ||
+ | // routeString is a string starting with / and | ||
+ | // callback includes at least two params: (request, response) | ||
+ | app.get('/', (req, res) => { | ||
+ | // intercepts requests to localhost:PORT/, and renders our index page | ||
+ | // __dirname is a special nodejs variable containing the current directory path | ||
+ | res.sendFile(`${__dirname}/public/index.html`); | ||
+ | }); | ||
+ | |||
+ | // match all file requests to public folder (this allows js/css links in your html) | ||
+ | // this means to get to our index page, you can either go to localhost:PORT/ or localhost:/PORT/index.html | ||
+ | app.get('/:filename', (req, res) => { | ||
+ | res.sendFile(`${__dirname}/public/${req.params.filename}`); | ||
+ | }); | ||
+ | </source> | ||
+ | |||
+ | Note that in that last route, ''':filename''' is a parameter of the route, which we will go into a little more depth in in the next route. | ||
+ | |||
+ | That's all there is to it! | ||
+ | |||
+ | Next we're going to develop a route that will handle getting or creating a new game. This will get a little hairy, as we will be dealing with async functions, API calls, and MySQL queries all in one function. | ||
+ | |||
+ | <source lang="js"> | ||
+ | // route to match requests to get an existing or create a new blackjack game | ||
+ | // the /:gameName/ is a variable path. This means this route will match to | ||
+ | // /game/example1/getOrCreate | ||
+ | // /game/randomName/getOrCreate | ||
+ | // but will not match to | ||
+ | // /game/name/otherthing/getOrCreate | ||
+ | // the gameName is accessible via req.params.gameName | ||
+ | // Also note that our route has a callback which is async- | ||
+ | // this means we can use await inside our callback, as opposed to having nested callbacks | ||
+ | app.get('/game/:gameName/getOrCreate', async (req, res) => { | ||
+ | |||
+ | // establish our SQL query, using ? as our query parameters | ||
+ | let qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = ?'; | ||
+ | // execute the query- note we are using 'await query', instead of conn.query(qString, params, callback) | ||
+ | // the second parameter is an array of values to replace ? in our query, in order of use in the query | ||
+ | const response = await query(qString, [req.params.gameName]); | ||
+ | // if there is an empty array as response, our query returned 0 rows | ||
+ | if (response.length == 0) { | ||
+ | // here, we are using axios to make an API request to our deckOfCardsAPI. | ||
+ | // note that the response content (JSON) is in the return from axios.get object, at the data key. | ||
+ | // that's why we must 'await' the response from the request before accessing the .data | ||
+ | const newDeckData = (await axios.get(`${config.apiEndpoint}/new/shuffle/?deck_count=1`)).data; | ||
+ | |||
+ | // grab the deck id | ||
+ | const deckId = newDeckData.deck_id; | ||
+ | |||
+ | // build our mysql query to save our deck id | ||
+ | qString = 'INSERT INTO games (gameName, deckId) VALUES (?, ?)'; | ||
+ | |||
+ | // save the new game to the DB | ||
+ | await query(qString, [req.params.gameName, deckId]); | ||
+ | |||
+ | // the game starts with each player having two cards. | ||
+ | // each card must be drawn before we draw the next card, so we await the finish of each draw before drawing the next card | ||
+ | await drawCardToHand(deckId, 'player'); | ||
+ | await drawCardToHand(deckId, 'dealer'); | ||
+ | await drawCardToHand(deckId, 'player'); | ||
+ | await drawCardToHand(deckId, 'dealer'); | ||
+ | |||
+ | // get each player's pile of cards | ||
+ | const playerPileData = await getPileData(deckId, 'player'); | ||
+ | const dealerPileData = await getPileData(deckId, 'dealer'); | ||
+ | |||
+ | // return the appropriate game data to the client | ||
+ | res.json({ | ||
+ | gameName: req.params.gameName, | ||
+ | deckId: deckId, | ||
+ | gamesWon: 0, | ||
+ | gamesLost: 0, | ||
+ | dealerPile: dealerPileData, | ||
+ | playerPile: playerPileData, | ||
+ | scores: {} | ||
+ | }); | ||
+ | } else { | ||
+ | // the game already existed in the database, so we just need to get each player's respective pile | ||
+ | const deckId = response[0].deckId; | ||
+ | const playerPileData = await getPileData(deckId, 'player'); | ||
+ | const dealerPileData = await getPileData(deckId, 'dealer'); | ||
+ | |||
+ | // ... is the spread operator | ||
+ | // return relevant game data to client | ||
+ | res.json({ | ||
+ | ...response[0], | ||
+ | dealerPile: dealerPileData, | ||
+ | playerPile: playerPileData, | ||
+ | scores: {} | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | </source> | ||
+ | |||
+ | Make sure to read through this function and thoroughly understand what it does- if you have questions on the syntax, API response schema, or especially how '''async''', '''query''', or '''axios.get''' work you should figure that out now. | ||
+ | |||
+ | All that's left is a the next two routes: one to draw a card to a hand, and one to end a game. These routes should make sense to you if you understand what's going on in the previous route. | ||
+ | |||
+ | <source lang="js"> | ||
+ | // respond to requests to draw cards | ||
+ | app.get('/game/:deckId/draw/:player', async (req, res) => { | ||
+ | // draw the card | ||
+ | await drawCardToHand(req.params.deckId, req.params.player); | ||
+ | |||
+ | // return the appropriate player's pile data | ||
+ | res.json(await getPileData(req.params.deckId, req.params.player)); | ||
+ | }); | ||
+ | |||
+ | // respond to the end of a game | ||
+ | app.get('/game/:gameName/endGame/:winner', async (req, res) => { | ||
+ | |||
+ | // make api request for a new deck id | ||
+ | const newDeckData = (await axios.get(`${config.apiEndpoint}/new/shuffle/?deck_count=1`)).data; | ||
+ | const deckId = newDeckData.deck_id; | ||
+ | |||
+ | let qString; | ||
+ | // generate query to update relevant row in DB with a new deckId and update games won/lost | ||
+ | if (req.params.winner == 'player') { | ||
+ | qString = 'UPDATE games SET deckId = ?, gamesWon = gamesWon + 1 WHERE gameName = ?'; | ||
+ | } else { | ||
+ | qString = 'UPDATE games SET deckId = ?, gamesLost = gamesLost + 1 WHERE gameName = ?'; | ||
+ | } | ||
+ | // update row, then get players new cards | ||
+ | await query(qString, [deckId, req.params.gameName]); | ||
+ | await drawCardToHand(deckId, 'player'); | ||
+ | await drawCardToHand(deckId, 'dealer'); | ||
+ | await drawCardToHand(deckId, 'player'); | ||
+ | await drawCardToHand(deckId, 'dealer'); | ||
+ | |||
+ | // get player piles | ||
+ | const playerPileData = await getPileData(deckId, 'player'); | ||
+ | const dealerPileData = await getPileData(deckId, 'dealer'); | ||
+ | |||
+ | // get game data from DB | ||
+ | qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = ?'; | ||
+ | const response = await query(qString, [req.params.gameName]); | ||
+ | |||
+ | // return game data to client | ||
+ | res.json({ | ||
+ | gameName: req.params.gameName, | ||
+ | deckId: deckId, | ||
+ | gamesWon: response[0].gamesWon, | ||
+ | gamesLost: response[0].gamesLost, | ||
+ | dealerPile: dealerPileData, | ||
+ | playerPile: playerPileData, | ||
+ | scores: {} | ||
+ | }); | ||
+ | }); | ||
+ | </source> | ||
+ | |||
+ | Finally, we're going to define a few helper functions we used in the previous three routes. | ||
+ | |||
+ | <source lang="js"> | ||
+ | // make api call to draw a card to a specific hand | ||
+ | // note- hand should either be 'dealer' or 'player' | ||
+ | async function drawCardToHand(deckId, hand) { | ||
+ | // draw card from deck | ||
+ | const drawResponse = (await axios.get(`${config.apiEndpoint}/${deckId}/draw/?count=1`)).data; | ||
+ | |||
+ | // place card in appropriate hand | ||
+ | await axios.get(`${config.apiEndpoint}/${deckId}/pile/${hand}/add/?cards=${drawResponse.cards[0].code}`); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // make api call to get specified hand data | ||
+ | async function getPileData(deckId, hand) { | ||
+ | const res = (await axios.get(`${config.apiEndpoint}/${deckId}/pile/${hand}/list`)).data; | ||
+ | return res.piles[hand]; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | That's our API built! Now all that's left is the code to start our server. | ||
+ | |||
+ | <source lang="js"> | ||
+ | // run our express server | ||
+ | app.listen(config.PORT, () => { | ||
+ | console.log(`Server running on port ${config.PORT}`); | ||
+ | }); | ||
+ | </source> | ||
=== Python API Server (Ignore if you set up the NodeJS API Server === | === Python API Server (Ignore if you set up the NodeJS API Server === | ||
− | + | This section will be dedicated to developing our blackjack client API. The purpose of this API is threefold: to interface between the client and requests for web resources (linked stylesheets/scripts, serving webpages and static assets), interfacing between the client and the AWS RDS instance, and interfacing between the client and the deckofcards API. | |
+ | |||
+ | The first thing you'll need to do is install packages to assist in development. Open up a terminal in the root directory of your project and execute | ||
+ | |||
+ | <source lang="bash"> | ||
+ | pip install bottle mysql-connector-python requests | ||
+ | </source> | ||
+ | |||
+ | These are needed for the following: | ||
+ | |||
+ | * [https://pypi.org/project/requests/ requests], for making API calls from our server | ||
+ | * [https://pypi.org/project/mysql-connector-python/ mysql-connector-python], for making MySQL queries against a database | ||
+ | * [http://bottlepy.org/docs/dev/ bottle], for intercepting and handling requests to our server | ||
+ | |||
+ | All four of these packages are well reputed, well maintained, and well documented, so you should have no trouble finding resolutions to any issues you have with them. | ||
+ | |||
+ | Now that we have configured our node project, we will assemble our server.py code. | ||
+ | |||
+ | For the following sections, we will be diving into server-side python. If any of the syntax is unfamiliar to you, please check out the or look up any of the relevant syntax to help clarify what's going on. | ||
+ | |||
+ | Add the following lines to import the required packages to our server: | ||
+ | |||
+ | <source lang="py"> | ||
+ | # import appropriate modules | ||
+ | |||
+ | # bottle is the module which will manage our API routes/requests | ||
+ | from bottle import route, run, static_file | ||
+ | |||
+ | # requests handles external API calls | ||
+ | import requests | ||
+ | |||
+ | # mysql connector allows us to connect to a mysql database | ||
+ | import mysql.connector | ||
+ | |||
+ | # allows us to utilize our config file | ||
+ | import json | ||
+ | |||
+ | # load our config json file into a python dictionary | ||
+ | config = json.loads(open('config.json').read()) | ||
+ | </source> | ||
+ | |||
+ | Next we'll set up our database connection: | ||
+ | |||
+ | <source lang="py"> | ||
+ | # establish mysql connection | ||
+ | db = mysql.connector.connect( | ||
+ | host=config['mysqlHost'], | ||
+ | user=config['mysqlUser'], | ||
+ | passwd=config['mysqlPassword'], | ||
+ | database=config['mysqlDatabase'] | ||
+ | ) | ||
+ | |||
+ | print('Connection established') | ||
+ | |||
+ | # the cursor is used to query the database | ||
+ | cursor = db.cursor() | ||
+ | </source> | ||
+ | |||
+ | Than's all the setup we need! Now we can dive into our API routes. | ||
+ | |||
+ | An API is just a collection of routes and functions used to handle those routes. Essentially, we tell our server that when someone makes a request to '''www.myurl.com/someEndpoint''' we want to execute function x, and when someone makes a request to '''www.myurl.com/anotherEndpoint''' we want to execute function y. | ||
+ | |||
+ | As you may recall, the first purpose of our API server is to send files to the client, so we will set up two routes for that purpose: the first is so that when someone navigates to our index url ('''www.myurl.com/''') we serve our '''index.html''' file, and the second is to serve all files in the public folder (this is mainly to support HTML link attributes). | ||
+ | |||
+ | <source lang="py"> | ||
+ | # bottle routes follow this syntax: | ||
+ | # @route is a decorator, saying the following function should be called | ||
+ | # when we make a request to the specified string. | ||
+ | # this route matches the index request (i.e. www.google.com/) | ||
+ | @route('/') | ||
+ | def index(): | ||
+ | # serve a static file, which is hosted in the subdirectory public | ||
+ | return static_file('index.html', root='public') | ||
+ | |||
+ | # you can add a parameter to the path with <PARAM> | ||
+ | # so this route will match yourUrl/exampleRoute, yourUrl/anotherRoute | ||
+ | # but it will not match with yourUrl/exampleIsBad/becauseHasTwoParams | ||
+ | # note the name of the parameter is then passed into the handler function | ||
+ | # this route sends our linked js/css files to the client | ||
+ | @route('/<filename>') | ||
+ | def static(filename): | ||
+ | return static_file(filename, root='public') | ||
+ | </source> | ||
+ | |||
+ | That's all there is to setting up our file server. | ||
+ | |||
+ | Next we're going to develop a route that will handle getting or creating a new game. This will get a little less clean, since we have to deal with both the MySQL database and the deckofcards API. | ||
+ | |||
+ | <source lang="py"> | ||
+ | # route to match requests to get an existing or create a new blackjack game | ||
+ | @route('/game/<gameName>/getOrCreate') | ||
+ | def getOrCreate(gameName): | ||
+ | # create our sql query, using %s for query parameters | ||
+ | qString = "SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = %s" | ||
+ | |||
+ | # values is our parameter tuple. we use a comma to force values to be a tuple even though we only have one parameter | ||
+ | values = (gameName, ) | ||
+ | |||
+ | # execute the query with parameters | ||
+ | cursor.execute(qString, values) | ||
+ | |||
+ | # capture the result of the query | ||
+ | result = cursor.fetchall() | ||
+ | |||
+ | # no items in result means we need to make a new game in the API and new mysql entry | ||
+ | if len(result) == 0: | ||
+ | # make an API call to request a new deck | ||
+ | res = requests.get('{}/new/shuffle/?deck_count=1'.format(config['apiEndpoint'])) | ||
+ | # convert the request body to a python dictionary | ||
+ | data = res.json() | ||
+ | |||
+ | qString = 'INSERT INTO games (gameName, deckId) VALUES (%s, %s)' | ||
+ | values = (gameName, data['deck_id']) | ||
+ | # insert the record of our new game series to the database | ||
+ | cursor.execute(qString, values) | ||
+ | |||
+ | # we must commit so that database insertions are carried out | ||
+ | db.commit() | ||
+ | |||
+ | # draw cards | ||
+ | drawCardToHand(data['deck_id'], 'player') | ||
+ | drawCardToHand(data['deck_id'], 'dealer') | ||
+ | drawCardToHand(data['deck_id'], 'player') | ||
+ | drawCardToHand(data['deck_id'], 'dealer') | ||
+ | |||
+ | # figure out who has which cards | ||
+ | playerPileData = getPileData(data['deck_id'], 'player') | ||
+ | dealerPileData = getPileData(data['deck_id'], 'dealer') | ||
+ | |||
+ | # return game configuration to the client | ||
+ | # note scores are empty since they are computed by the client | ||
+ | return { | ||
+ | "gameName": gameName, | ||
+ | "deckId": data['deck_id'], | ||
+ | "gamesWon": 0, | ||
+ | "gamesLost": 0, | ||
+ | "dealerPile": dealerPileData, | ||
+ | "playerPile": playerPileData, | ||
+ | "scores": {} | ||
+ | } | ||
+ | else: | ||
+ | # game already exists in the database, so load it | ||
+ | # note that fetchall returns a list of tuples, so we must use numeric access | ||
+ | deck_id = result[0][1] | ||
+ | |||
+ | playerPileData = getPileData(deck_id, 'player') | ||
+ | dealerPileData = getPileData(deck_id, 'dealer') | ||
+ | |||
+ | return { | ||
+ | "gameName": gameName, | ||
+ | "deckId": deck_id, | ||
+ | "gamesWon": result[0][2], | ||
+ | "gamesLost": result[0][3], | ||
+ | "dealerPile": dealerPileData, | ||
+ | "playerPile": playerPileData, | ||
+ | "scores": {} | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | Make sure to read through this function and thoroughly understand what it does- if you have questions on the syntax, API response schema, or how the database query process is, read up on the documentation before moving on. | ||
+ | |||
+ | Just two routes left: one to draw a card to a hand, and one to end a game. These routes should make sense to you if you understand what's going on in the previous route. | ||
+ | |||
+ | <source lang="py"> | ||
+ | # respond to request to draw a card | ||
+ | @route('/game/<deckId>/draw/<player>') | ||
+ | def draw(deckId, player): | ||
+ | # draw the card | ||
+ | drawCardToHand(deckId, player) | ||
+ | # return the appropriate player pile | ||
+ | return getPileData(deckId, player) | ||
+ | |||
+ | # respond to the end of a game | ||
+ | @route('/game/<gameName>/endGame/<winner>') | ||
+ | def endGame(gameName, winner): | ||
+ | # get new deck | ||
+ | res = requests.get('{}/new/shuffle/?deck_count=1'.format(config['apiEndpoint'])) | ||
+ | data = res.json() | ||
+ | |||
+ | # generate query so that appropriate database cells are updated | ||
+ | qString = '' | ||
+ | if winner == 'player': | ||
+ | qString = 'UPDATE games SET deckId = %s, gamesWon = gamesWon + 1 WHERE gameName = %s' | ||
+ | else: | ||
+ | qString = 'UPDATE games SET deckId = %s, gamesLost = gamesLost + 1 WHERE gameName = %s' | ||
+ | values = (data['deck_id'], gameName) | ||
+ | cursor.execute(qString, values) | ||
+ | db.commit() | ||
+ | |||
+ | drawCardToHand(data['deck_id'], 'player') | ||
+ | drawCardToHand(data['deck_id'], 'dealer') | ||
+ | drawCardToHand(data['deck_id'], 'player') | ||
+ | drawCardToHand(data['deck_id'], 'dealer') | ||
+ | |||
+ | playerPileData = getPileData(data['deck_id'], 'player') | ||
+ | dealerPileData = getPileData(data['deck_id'], 'dealer') | ||
+ | |||
+ | # get updated game data from DB | ||
+ | qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = %s' | ||
+ | values = (gameName, ) | ||
+ | cursor.execute(qString, values) | ||
+ | |||
+ | # fetchone is like fetchall but returns a single tuple | ||
+ | result = cursor.fetchone() | ||
+ | |||
+ | return { | ||
+ | "gameName": gameName, | ||
+ | "deckId": data['deck_id'], | ||
+ | "gamesWon": result[2], | ||
+ | "gamesLost": result[3], | ||
+ | "dealerPile": dealerPileData, | ||
+ | "playerPile": playerPileData, | ||
+ | "scores": {} | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | Finally, we're going to define a few helper functions we used in the previous three routes: | ||
+ | |||
+ | <source lang="py"> | ||
+ | # helper function to draw a card to the appropriate hand | ||
+ | def drawCardToHand(deckId, hand): | ||
+ | # draw card from deck | ||
+ | res = requests.get('{}/{}/draw/?count=1'.format(config['apiEndpoint'],deckId)) | ||
+ | data = res.json() | ||
+ | # move card to appropriate pile (hand) | ||
+ | requests.get('{}/{}/pile/{}/add/?cards={}'.format(config['apiEndpoint'],deckId, hand, data['cards'][0]['code'])) | ||
+ | return | ||
+ | |||
+ | # helper function to get specified hand data | ||
+ | def getPileData(deckId, hand): | ||
+ | res = requests.get('{}/{}/pile/{}/list'.format(config['apiEndpoint'], deckId, hand)) | ||
+ | data = res.json() | ||
+ | return data['piles'][hand] | ||
+ | </source> | ||
+ | |||
+ | That's our API built! Now all that's left is the code to start our server. | ||
+ | |||
+ | <source lang="py"> | ||
+ | # run the bottle API server | ||
+ | run(host='localhost', port=config['PORT']) | ||
+ | </source> | ||
=== Client Webpage === | === Client Webpage === | ||
+ | |||
+ | If you know nothing about HTML and CSS, I highly recommend you look for a decent primer on those and then come back ([https://www.w3schools.com/ w3 schools is a great resource]). | ||
+ | |||
+ | We've already developed a boilerplate HTML page and a very basic CSS stylesheet to go along with it for this project, so simply copy the contents into their respective files. | ||
+ | |||
+ | <source lang="html"> | ||
+ | <!-- index.html --> | ||
+ | <!DOCTYPE html> | ||
+ | <html> | ||
+ | <head> | ||
+ | <!--Standard HTML setup stuff, don't worry about what this does, just include it--> | ||
+ | <meta charset="utf-8" /> | ||
+ | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
+ | <!--Title of the page- shows up in the browser tab--> | ||
+ | <title>Blackjack</title> | ||
+ | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
+ | </head> | ||
+ | <body> | ||
+ | <!--Login information- hidden while in game--> | ||
+ | <div id="login-wrapper"> | ||
+ | <div class="input-label">Deck Name</div> | ||
+ | <input type="text" placeholder="myBlackjackDeck" id="deckNameInput" /> | ||
+ | <div id="enter-game-button" class="button">Create/Load Game</div> | ||
+ | </div> | ||
+ | <!--Game information- hidden while logging in--> | ||
+ | <div id="game-wrapper" class="hidden"> | ||
+ | <div id="dealer-and-player-wrapper"> | ||
+ | <div id="dealer-wrapper"> | ||
+ | <div class="title">Dealer Hand</div> | ||
+ | <div id="dealer-card-wrapper"> | ||
+ | <div class="card">Placeholder Card</div> | ||
+ | </div> | ||
+ | </div> | ||
+ | <div id="player-wrapper"> | ||
+ | <div class="title">Player Hand</div> | ||
+ | <div id="player-card-wrapper"> | ||
+ | <div class="card">Placeholder Card</div> | ||
+ | </div> | ||
+ | </div> | ||
+ | </div> | ||
+ | <div id="actions-wrapper"> | ||
+ | <div id="hit-button">Hit</div> | ||
+ | <div id="stand-button">Stand</div> | ||
+ | </div> | ||
+ | <div id="info-banner">Games won: 0, Games lost: 0</div> | ||
+ | </div> | ||
+ | </body> | ||
+ | <!--Load scripts and styles at the end- ensures elements are loaded before trying to add event listeners, and speeds up l=page load times--> | ||
+ | <link rel="stylesheet" type="text/css" media="screen" href="index.css" /> | ||
+ | <script src="index.js"></script> | ||
+ | </html> | ||
+ | </source> | ||
+ | |||
+ | <source lang="css"> | ||
+ | /* index.css */ | ||
+ | .input-label { | ||
+ | text-align: center; | ||
+ | font-size: 20px; | ||
+ | margin: 20px; | ||
+ | } | ||
+ | |||
+ | input[type="text"] { | ||
+ | margin: 20px; | ||
+ | /* | ||
+ | Calc is a special css property- | ||
+ | it allows dynamic calcualtion of a property. | ||
+ | Used here to ensure input is centered in screen after accounting for margin & border | ||
+ | */ | ||
+ | width: calc(100% - 44px); | ||
+ | text-align: center; | ||
+ | font-size: 24px; | ||
+ | } | ||
+ | |||
+ | .hidden, #game-wrapper.hidden { | ||
+ | display: none; | ||
+ | } | ||
+ | |||
+ | #dealer-and-player-wrapper { | ||
+ | display: flex; | ||
+ | flex-direction: column; | ||
+ | } | ||
+ | |||
+ | #player-wrapper, #dealer-wrapper { | ||
+ | display: flex; | ||
+ | flex-direction: column; | ||
+ | } | ||
+ | |||
+ | #actions-banner { | ||
+ | display: flex; | ||
+ | flex-direction: row; | ||
+ | justify-content: space-around; | ||
+ | } | ||
+ | |||
+ | #actions-banner > div { | ||
+ | margin: 20px 30px 20px 30px; | ||
+ | } | ||
+ | |||
+ | .title { | ||
+ | color: rgb(32, 32, 32); | ||
+ | font-size: 24px; | ||
+ | } | ||
+ | |||
+ | .subheading { | ||
+ | color: rgb(32, 32, 32); | ||
+ | font-size: 20px; | ||
+ | } | ||
+ | |||
+ | .card { | ||
+ | background-color: rgb(137, 137, 255); | ||
+ | padding: 20px; | ||
+ | border-radius: 10px; | ||
+ | text-align: center; | ||
+ | color: rgb(32, 32, 32); | ||
+ | font-size: 18px; | ||
+ | margin: 15px; | ||
+ | } | ||
+ | |||
+ | #hit-button, #stand-button, #enter-game-button { | ||
+ | background-color: rgb(255, 90, 68); | ||
+ | border-radius: 5px; | ||
+ | text-align: center; | ||
+ | font-size: 24px; | ||
+ | padding: 10px; | ||
+ | margin: 10px 0px 0px 0px; | ||
+ | color: rgb(32,32,32); | ||
+ | cursor: pointer; | ||
+ | } | ||
+ | |||
+ | #hit-button:hover, #stand-button:hover, #enter-game-button:hover { | ||
+ | background-color: rgb(190, 61, 44); | ||
+ | border-radius: 5px; | ||
+ | cursor: pointer; | ||
+ | } | ||
+ | </source> | ||
=== Client Script === | === Client Script === | ||
+ | |||
+ | This section will be dedicated to constructing the client script for our application. The client script has three main responsibilities: maintaining data related to application instance state, updating the DOM (the browser window), and interacting with our client API. All of the following modifications in this section will be made to our index.js file. | ||
+ | |||
+ | The first thing we'll do is set up our event listeners and global constants: | ||
+ | |||
+ | <source lang="js> | ||
+ | // event listeners, makes our HTML Div elements respond to click | ||
+ | document.getElementById('enter-game-button').addEventListener('click', () => createOrJoinGame()); | ||
+ | document.getElementById('hit-button').addEventListener('click', () => hit()); | ||
+ | document.getElementById('stand-button').addEventListener('click', () => stand()); | ||
+ | |||
+ | // use const instead of magic strings when possible | ||
+ | const DEALER = 'dealer'; | ||
+ | const PLAYER = 'player'; | ||
+ | |||
+ | // initialize global game variable | ||
+ | // note that after initialization, the game data will have the following relevant properties: | ||
+ | /* | ||
+ | { | ||
+ | gamesWon: number, | ||
+ | gamesLost: number, | ||
+ | gameId: string, | ||
+ | gameName: string, | ||
+ | scores: { | ||
+ | player: number, | ||
+ | dealer: number | ||
+ | }, | ||
+ | playerPile: { | ||
+ | cards: [ | ||
+ | { | ||
+ | value: 0-9 || JACK || QUEEN || KING || ACE, | ||
+ | suit: string, | ||
+ | } | ||
+ | ] | ||
+ | }, | ||
+ | dealerPile: object with same format as playerPile | ||
+ | } | ||
+ | */ | ||
+ | let game = {}; | ||
+ | </source> | ||
+ | |||
+ | There's not much to talk about here, so we'll move on to defining the three functions we want to use to handle our three event lsiteners: | ||
+ | |||
+ | <source lang="js"> | ||
+ | // response to player hitting 'enter-game-button' | ||
+ | // requests game for a given game name from our API server | ||
+ | // note that this is an async functions. This means that within this function, we can use the keyword 'await'. | ||
+ | // this allows us to minimize the number of callbacks we have to use | ||
+ | async function createOrJoinGame() { | ||
+ | // get deck name from input | ||
+ | const gameName = document.getElementById('deckNameInput').value; | ||
+ | |||
+ | // make API call to server to either continue an existing game or create a new game | ||
+ | const gameData = await fetch(`/game/${gameName}/getOrCreate`); | ||
+ | |||
+ | // get JSON body of API response | ||
+ | game = await gameData.json(); | ||
+ | |||
+ | // hide the login details, show the game details | ||
+ | document.getElementById('login-wrapper').classList.add('hidden'); | ||
+ | document.getElementById('game-wrapper').classList.remove('hidden'); | ||
+ | |||
+ | // begin the game | ||
+ | initGame(); | ||
+ | }; | ||
+ | |||
+ | // response to player clicking 'hit-button' | ||
+ | // gets card from API server, determines if player has lost based on newly drawn card | ||
+ | async function hit() { | ||
+ | // draw card from API | ||
+ | const playerPile = await fetch(`/game/${game.deckId}/draw/player`); | ||
+ | // update player pile with the pile retrieved from the API | ||
+ | // note that playerPile.json() is an async function, so we have to await it before asking for .piles.player | ||
+ | game.playerPile = await playerPile.json(); | ||
+ | |||
+ | // update display with new card | ||
+ | updateGameDisplay(); | ||
+ | |||
+ | // recalculate score | ||
+ | game.scores[PLAYER] = scorePlayer(PLAYER); | ||
+ | |||
+ | // use setTimeout to allow browser to re-draw cards before checking for a loss | ||
+ | // don't worry about this workaround, it's probably beyond the scope of what you need to care about | ||
+ | setTimeout(() => { | ||
+ | |||
+ | // determine if player automatically wins or loses | ||
+ | if (game.scores[PLAYER] > 21) { | ||
+ | alert(`You lose, score = ` + game.scores[PLAYER]); | ||
+ | endGame(DEALER); | ||
+ | } else if (game.scores[PLAYER] == 21) { | ||
+ | alert('You win, score = 21'); | ||
+ | endGame(PLAYER); | ||
+ | } | ||
+ | |||
+ | }, 1); | ||
+ | }; | ||
+ | |||
+ | // response to player clicking 'stand-button' | ||
+ | // causes dealer to draw cards until satisfied, then calculates a winner | ||
+ | async function stand() { | ||
+ | |||
+ | // most basic AI possible- dealer should draw if they have less than 17 points | ||
+ | while(game.scores[DEALER] < 17) { | ||
+ | // request new card for dealer from API | ||
+ | const dealerPile = await fetch(`/game/${game.deckId}/draw/dealer`); | ||
+ | |||
+ | // update dealer pile | ||
+ | game.dealerPile = await dealerPile.json(); | ||
+ | |||
+ | // update dealer score | ||
+ | game.scores[DEALER] = scorePlayer(DEALER); | ||
+ | |||
+ | updateGameDisplay(); | ||
+ | } | ||
+ | |||
+ | |||
+ | // determine game winner | ||
+ | setTimeout(() => { | ||
+ | |||
+ | if (game.scores[DEALER] > 21 || game.scores[DEALER] < game.scores[PLAYER]) { | ||
+ | alert(`You win. ${game.scores[PLAYER]} to ${game.scores[DEALER]}`); | ||
+ | endGame(PLAYER); | ||
+ | } else { | ||
+ | alert(`You lose. ${game.scores[PLAYER]} to ${game.scores[DEALER]}`); | ||
+ | endGame(DEALER); | ||
+ | } | ||
+ | |||
+ | }, 1); | ||
+ | }; | ||
+ | </source> | ||
+ | |||
+ | Here you'll see interaction with our API: each '''fetch''' will make a GET request to our client API- you should note that the route specified in each fetch request matches with a route we've set up on our server. | ||
+ | |||
+ | Next we'll define the helper functions that we use in our three event responders | ||
+ | |||
+ | <source lang="js"> | ||
+ | // calcualtes player score | ||
+ | function scorePlayer(player) { | ||
+ | // determine if we are claculating a score for the player or the dealer | ||
+ | const pile = player == PLAYER ? game.playerPile.cards : game.dealerPile.cards; | ||
+ | |||
+ | let score = 0; | ||
+ | // count aces, since they can be 1's or 11's | ||
+ | let aceCount = 0; | ||
+ | |||
+ | // fancy for loop, basically passes card in instead of needing to use pile[i] like a traditional for loop | ||
+ | pile.forEach((card) => { | ||
+ | |||
+ | // if the card value is numeric, we can directly add it | ||
+ | if (!isNaN(card.value)) { | ||
+ | // API represents 10s as 0s, so account for that in our addition | ||
+ | score += card.value == 0 ? 10 : parseInt(card.value); | ||
+ | // deal with face cards | ||
+ | } else if (card.value == 'KING' || card.value == 'QUEEN' || card.value =='JACK') { | ||
+ | score += 10; | ||
+ | // otherwise we've got an ace- don't forget to count it. | ||
+ | // we assume we want to count an ace as an 11 | ||
+ | } else { | ||
+ | aceCount += 1; | ||
+ | score += 11; | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | // if we have aces and our score is too high, we want to count them as 1s instead of 11s | ||
+ | while (score > 21 && aceCount > 0) { | ||
+ | score -= 10; | ||
+ | aceCount -= 1; | ||
+ | } | ||
+ | |||
+ | return score; | ||
+ | } | ||
+ | |||
+ | // finished with our game | ||
+ | async function endGame(gameWinner) { | ||
+ | |||
+ | clearGameDisplay(); | ||
+ | |||
+ | // make API call to get a new deck and new starting draw | ||
+ | const gameData = await fetch(`/game/${game.gameName}/endGame/${gameWinner}`); | ||
+ | |||
+ | game = await gameData.json(); | ||
+ | |||
+ | initGame(); | ||
+ | } | ||
+ | |||
+ | // set up scores/display after getting a new game object | ||
+ | function initGame() { | ||
+ | |||
+ | game.scores[PLAYER] = scorePlayer(PLAYER); | ||
+ | game.scores[DEALER] = scorePlayer(DEALER); | ||
+ | |||
+ | updateGameDisplay(); | ||
+ | } | ||
+ | |||
+ | // update visual components of the game | ||
+ | function updateGameDisplay() { | ||
+ | // grab our card wrapper elements | ||
+ | const pCardWrapper = document.getElementById('player-card-wrapper'); | ||
+ | const dCardWrapper = document.getElementById('dealer-card-wrapper'); | ||
+ | |||
+ | // remove all cards from them | ||
+ | pCardWrapper.innerHTML = ''; | ||
+ | dCardWrapper.innerHTML = ''; | ||
+ | |||
+ | // give dealer their cards | ||
+ | // draw new div elements for each card | ||
+ | game.dealerPile.cards.forEach((c) => { | ||
+ | // create div element | ||
+ | const card = document.createElement('div'); | ||
+ | // give div a class 'card' | ||
+ | card.classList.add('card'); | ||
+ | |||
+ | // give the card its text content, accounting for the 0 represented as a 10 thing | ||
+ | card.innerHTML = c.value == 0 ? '10 of ' + c.suit : c.value + ' of ' + c.suit; | ||
+ | |||
+ | // add the card to the appropriate wrapper element | ||
+ | dCardWrapper.appendChild(card); | ||
+ | }); | ||
+ | |||
+ | // give player their cards | ||
+ | game.playerPile.cards.forEach((c) => { | ||
+ | |||
+ | const card = document.createElement('div'); | ||
+ | |||
+ | card.classList.add('card'); | ||
+ | |||
+ | card.innerHTML = c.value == 0 ? '10 of ' + c.suit : c.value + ' of ' + c.suit; | ||
+ | |||
+ | pCardWrapper.appendChild(card); | ||
+ | }); | ||
+ | |||
+ | // update the score information | ||
+ | document.getElementById('info-banner').innerHTML = `Games Won: ${game.gamesWon}, Games Lost: ${game.gamesLost}`; | ||
+ | } | ||
+ | |||
+ | // clears cards while loading the next game, to minimize confusion | ||
+ | function clearGameDisplay() { | ||
+ | document.getElementById('player-card-wrapper').innerHTML = ''; | ||
+ | document.getElementById('dealer-card-wrapper').innerHTML = ''; | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | And that's all there is to it! Take some time to digest how this whole front-end application works, digest the syntax, look up things you don't understand. | ||
+ | |||
+ | === Testing and moving to server === | ||
+ | |||
+ | Now that all your code works, all that's left is to test it and get it running on a server. | ||
+ | |||
+ | To run your code, in a terminal '''cd''' to your project root and execute | ||
+ | |||
+ | <source lang="bash"> | ||
+ | npm start | ||
+ | </source> | ||
+ | |||
+ | You should see '''Server running on port 4000''' printed to the terminal. Now if you navigate to '''localhost:4000/''' in a browser window you should be able to play your blackjack game! | ||
+ | |||
+ | Test to make sure everything is working properly, and then get all your code up to git. | ||
+ | |||
+ | To get your code running on a server, follow the directions [[AWS Lightsail|at this tutorial]]. | ||
== Authors == | == Authors == |
Latest revision as of 00:37, 15 February 2019
Contents
Overview
This tutorial will be the guide for an intro to Web Development project building Blackjack to gain a basic understanding of the major building blocks of web applications: APIs, MySQL databases, Client Webpages, Cloud Servers and Github.
Here's what the final project will look like (it's terrible, but sufficient for our needs):
Each project will utilize the following major pieces:
- The deckofcardsapi will be utilized to store deck state, as well as manage drawing cards and tracking player/dealer hands
- An AWS MySQL instance will be utilized to store data about a series of games, including current deckId for use in the external API, as well as keeping track of total wins/losses
- A client webpage will be constructed via HTML/CSS/JS
- An API will be built utilizing either Python or NodeJS, which will manage interaction between the client and MySQL/deckofcardsapi
There are two main paths to complete the API portion of this project: python and nodejs. If you have significant experience with python, feel free to use it as your API language, however otherwise we recommend you build a NodeJS server so that you don't have to gain familiarity in both Javascript and Python to complete this project.
Overview of the project specification
Each series of Blackjack games will be connected to a gameName. This name is what will allow us to keep track of how many wins/losses you have for that series, and will keep track of a deckId for the current game. This Id is what the deckofcards API uses as an identifier for its decks. This will allow us begin a game/series of games, close the browser, and continue right where we left off.
In a given browser session, the user will specify a gameName. This will send a request to our client API to either load an in progress game series, or create a new game series. Once we have loaded a game or created a new one, a user will have the option to either hit or stand. The user can continue to hit until their score reaches 21, at which point they win, or exceeds 21, in which case they lose. When they chose to stand, the dealer will draw cards until their score meets or exceeds 17, at which point a winner will be determined and a new game will be created.
There are four reasons in which the client webpage will interact with the client API:
- The client is requesting a file from the server
- A gameName is sent for a game series to be created or resumed
- A deckId and playerName is sent so that a card may be drawn to that player's hand
- A gameName and winnerName is sent so that a game can be completed and a new game begun
There are three reasons why we might interact with our MySQL database:
- We want to see if a gameName exists in our database already
- We want to insert a new game series into our database
- We want to update our database with a new deckId and increment gamesWon or gamesLost after the completion of a game
There are four reasons why we might interact with the deckofcards API:
- We want to generate a new, shuffled deck and retrieve a deckId
- We want to draw a card
- We want to add a drawn card to a player's hand (a 'pile' in the API's terminology)
- We want to retrieve a list of cards in a player's hand (a 'pile')
Though this seems like a lot, we will step through each piece an explain what is happening in detail to help simplify how everything works together.
Materials/Prerequisites
As always, you will be using Git. It is highly reccommended you use Git in the command line. There are really only five Git commands that matter:
# Copy git repository to computer
git clone <repository URL>
# see current repo data like which files have been modified since the last commit
git status
# add all files to the next commit
git add .
# join files together in one package
git commit -m "COMMIT_MESSAGE"
# send changes up to the server
git push origin master
We will be utilizing a variety of tools/technologies to accomplish this project.
If you are using Python, we expect you to have Python 3.x.x installed. You can check your python version with the command:
python --verision
If you are using NodeJs, we expect you to have installed a version of Node, which can be obtained here.
Find a Git repository that you can use. Feel free to use the one already established for your group, just locate everything in a subfolder (i.e. GroupRepo > Blackjack).
Gain a basic familiarity with the API we'll be using, (particularly the schema of the responses), deckofcardsapi
At this point you should complete the MySQL database on AWS tutorial, which can be found here.
Process
Setting up our MySQL Table
Open up MySQL Workbench and open your already-established connection to your RDS instance. Once you're connected, in the left sidebar under Schemas ensure your database is highlighted, so that any query you execute will be against your database.
We want to create our games table. Recall this table needs to track several properties: gamesWon, gamesLost, deckId, and gameName. We must also include an id column, which is what will distinguish each row from each other and act as our Primary Key.
Sample Table:
id gameName deckId gamesWon gamesLost ------------------------------------------ 1 game1 d643sj 12 9 2 myGame y73nd1 2 0
In MySQL workbench, create a new SQL tab for executing queries (this should be one of the icons in the upper right hand corner). Paste the following code and click on the lightning bolt icon to create the games table:
CREATE TABLE games (
id INT PRIMARY KEY AUTO_INCREMENT,
gameName VARCHAR(50) NOT NULL UNIQUE,
deckId VARCHAR(50),
gamesWon INT DEFAULT 0,
gamesLost INT DEFAULT 0
);
CREATE INDEX gameNameIndex ON games (gameName);
There are a few things to take note of here:
- AUTO_INCREMENT automatically increments the id column- this means we don't need to specify an id when adding a row to the table. It also means that if we delete a row of the table (say row 6) that index is gone forever- the next row will be created at row 7 even though only indexes 1-5 are the only indexes taken.
- VARCHAR(50) specifies that this column will contain a string of a maximum length of 50
- CREATE INDEX allows us to query against that column- now we can perform SQL searches using WHERE gameName = 'something'
Create another SQL tab and executing the following command should yield an empty table:
SELECT * FROM games;
-------------------------------------------
RESULT:
-------------------------------------------
id gameName deckId gamesWon gamesLost
NULL NULL NULL NULL NULL
Your database is all good to go now! After each game the table should populate, and may resemble the following:
Setting up our project directory
In this step we'll set up our files/folders for the project. The main concern here is we want to isolate any files which should be accessible through the API's file server from our code files. This will help ensure the security of our source code.
Create the following directory structure:
Repository Root
|-public
|- index.html
|- index.css
|- index.js
|- server.js/server.py
|- config.json
Any files we want our browser to be able to access, we will store exclusively in our public folder/subfolders, while our server code will remain outside that folder.
Our config.json is another best-practices thing we will do. In it we will store things like the root for our deckofcards API endpoint (to reduce the number of magic strings), as well as things we wish to keep secure (such as our MySQL connection information). In industry, we would probably tell git to ignore our config.json file and would manually upload it to the server, so that any secure information stored in it wouldn't be available to people who have visibility to your git repository, however that particular application is outside the scope of this tutorial.
We'll also go ahead and fill the config.json with the data we'll use to configure our application:
{
"PORT": 4000,
"apiEndpoint": "https://deckofcardsapi.com/api/deck",
"mysqlHost": "YOUR_RDS_INSTANCE_HOST",
"mysqlUser": "YOUR_DB_USER",
"mysqlPassword": "YOUR_DB_PASSWORD",
"mysqlDatabase": "YOUR_DB_NAME"
}
NodeJS API Server (Or skip to python if using python)
This section will be dedicated to developing our blackjack client API. The purpose of this API is threefold: to interface between the client and requests for web resources (linked stylesheets/scripts, serving webpages and static assets), interfacing between the client and the AWS RDS instance, and interfacing between the client and the deckofcards API.
The first thing you'll need to do is establish your node project and install packages to assist in development. Open up a terminal in the root directory of your project and execute
npm init
Step through the prompts, feel free to leave them all as default. You should now see a package.json file has been created in your project's directory. Next, we will install all the packages we need to facilitate our API. This will consist of:
- axios, for making API calls from our server
- mysql, for making MySQL queries against a database
- express, for intercepting and handling requests to our server
- body-parser, for parsing POST request bodies (not used in this project, but you will want this 9/10 times in API development).
All four of these packages are well reputed, well maintained, and well documented, and you should have no trouble finding resolutions to any issues you have with them. In the console, type:
npm install --save axios mysql express body-parser
If you look in your package.json, you should now see a section titles requirements which contains the four packages (and an associated minimum version number). You should also see a node_modules folder with hundred(s) of subfolders.
In the meantime, we will also want to ensure our start script is set correctly. Check the scripts' section of your package.json to ensure the start entry is specified:
"scripts": {
...
"start": "node server.js",
...
}
This will allow you to run your server.js file from the terminal using the command npm start.
The last thing you're going to want to do is ensure that you don't commit your package-lock.json or node_modules to git. If you haven't already, create a file called .gitignore and add the following entries:
**/node_modules/
**/package-lock.json
Now that we have configured our node project, we will assemble our server.js code.
For the following sections, we will be diving into server-side javascript. If any of the syntax is unfamiliar to you, please check out the or look up any of the relevant syntax to help clarify what's going on.
Add the following lines to import the required packages to our server:
// import appropriate modules
// mysql module, for mysql requests
const mysql = require('mysql');
// util, to allow us to use async/await
// util is built in to nodejs, so we don't have to install it
const utils = require('util');
// axios, to make API requests
const axios = require('axios');
// express, to maintain our API server
const express = require('express');
// API post body middleware
const bodyParser = require('body-parser');
// local json file with important data/global constants
const config = require('./config.json');
Notice that since our modules are packages, we simply specify the package name and node handles finding the appropriate code to import, but since our config file is something we've created, we must specify the full path to that file.
Next we'll finish setting up our server and database connection, as well as do a little trick to allow us to avoid using callbacks for all our MySQL calls. (if you don't know what I mean when I say callback, or are unfamiliar with the idea of asyncronous programming, check out this introduction to the asyncronous nature of javascript).
// initialize our API server
const app = express();
// POST body middleware (parses POST body to json for use in routes)
app.use(bodyParser.json());
// establish mysql connection settings
const conn = mysql.createConnection({
host: config.mysqlHost,
user: config.mysqlUser,
password: config.mysqlPassword,
database: config.mysqlDatabase
});
// make conn.query a promise, this allows us to use 'await' instead of a callback
// don't worry about the details of this, just include this line
// now instead of using 'conn.query(query, params, callback)' we'll use 'await query'
const query = utils.promisify(conn.query).bind(conn);
// connect to our mysql database
conn.connect((err) => {
if (err) {
console.log('Unable to connect to mysql');
throw err;
}
});
Than's all the setup we need! Now we can dive into our API routes.
An API is just a collection of routes and functions used to handle those routes. Essentially, we tell our server that when someone makes a request to www.myurl.com/someEndpoint we want to execute function x, and when someone makes a request to www.myurl.com/anotherEndpoint we want to execute function y.
As you may recall, the first purpose of our API server is to send files to the client, so we will set up two routes for that purpose: the first is so that when someone navigates to our index url (www.myurl.com/) we serve our index.html file, and the second is to serve all files in the public folder (this is mainly to support HTML link attributes).
// match base route to index.html
// this is a basic express route. the general format is app.get(routeString, callback)
// where 'get' is the HTTP method (i.e. GET/POST/PUT),
// routeString is a string starting with / and
// callback includes at least two params: (request, response)
app.get('/', (req, res) => {
// intercepts requests to localhost:PORT/, and renders our index page
// __dirname is a special nodejs variable containing the current directory path
res.sendFile(`${__dirname}/public/index.html`);
});
// match all file requests to public folder (this allows js/css links in your html)
// this means to get to our index page, you can either go to localhost:PORT/ or localhost:/PORT/index.html
app.get('/:filename', (req, res) => {
res.sendFile(`${__dirname}/public/${req.params.filename}`);
});
Note that in that last route, :filename is a parameter of the route, which we will go into a little more depth in in the next route.
That's all there is to it!
Next we're going to develop a route that will handle getting or creating a new game. This will get a little hairy, as we will be dealing with async functions, API calls, and MySQL queries all in one function.
// route to match requests to get an existing or create a new blackjack game
// the /:gameName/ is a variable path. This means this route will match to
// /game/example1/getOrCreate
// /game/randomName/getOrCreate
// but will not match to
// /game/name/otherthing/getOrCreate
// the gameName is accessible via req.params.gameName
// Also note that our route has a callback which is async-
// this means we can use await inside our callback, as opposed to having nested callbacks
app.get('/game/:gameName/getOrCreate', async (req, res) => {
// establish our SQL query, using ? as our query parameters
let qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = ?';
// execute the query- note we are using 'await query', instead of conn.query(qString, params, callback)
// the second parameter is an array of values to replace ? in our query, in order of use in the query
const response = await query(qString, [req.params.gameName]);
// if there is an empty array as response, our query returned 0 rows
if (response.length == 0) {
// here, we are using axios to make an API request to our deckOfCardsAPI.
// note that the response content (JSON) is in the return from axios.get object, at the data key.
// that's why we must 'await' the response from the request before accessing the .data
const newDeckData = (await axios.get(`${config.apiEndpoint}/new/shuffle/?deck_count=1`)).data;
// grab the deck id
const deckId = newDeckData.deck_id;
// build our mysql query to save our deck id
qString = 'INSERT INTO games (gameName, deckId) VALUES (?, ?)';
// save the new game to the DB
await query(qString, [req.params.gameName, deckId]);
// the game starts with each player having two cards.
// each card must be drawn before we draw the next card, so we await the finish of each draw before drawing the next card
await drawCardToHand(deckId, 'player');
await drawCardToHand(deckId, 'dealer');
await drawCardToHand(deckId, 'player');
await drawCardToHand(deckId, 'dealer');
// get each player's pile of cards
const playerPileData = await getPileData(deckId, 'player');
const dealerPileData = await getPileData(deckId, 'dealer');
// return the appropriate game data to the client
res.json({
gameName: req.params.gameName,
deckId: deckId,
gamesWon: 0,
gamesLost: 0,
dealerPile: dealerPileData,
playerPile: playerPileData,
scores: {}
});
} else {
// the game already existed in the database, so we just need to get each player's respective pile
const deckId = response[0].deckId;
const playerPileData = await getPileData(deckId, 'player');
const dealerPileData = await getPileData(deckId, 'dealer');
// ... is the spread operator
// return relevant game data to client
res.json({
...response[0],
dealerPile: dealerPileData,
playerPile: playerPileData,
scores: {}
});
}
});
Make sure to read through this function and thoroughly understand what it does- if you have questions on the syntax, API response schema, or especially how async, query, or axios.get work you should figure that out now.
All that's left is a the next two routes: one to draw a card to a hand, and one to end a game. These routes should make sense to you if you understand what's going on in the previous route.
// respond to requests to draw cards
app.get('/game/:deckId/draw/:player', async (req, res) => {
// draw the card
await drawCardToHand(req.params.deckId, req.params.player);
// return the appropriate player's pile data
res.json(await getPileData(req.params.deckId, req.params.player));
});
// respond to the end of a game
app.get('/game/:gameName/endGame/:winner', async (req, res) => {
// make api request for a new deck id
const newDeckData = (await axios.get(`${config.apiEndpoint}/new/shuffle/?deck_count=1`)).data;
const deckId = newDeckData.deck_id;
let qString;
// generate query to update relevant row in DB with a new deckId and update games won/lost
if (req.params.winner == 'player') {
qString = 'UPDATE games SET deckId = ?, gamesWon = gamesWon + 1 WHERE gameName = ?';
} else {
qString = 'UPDATE games SET deckId = ?, gamesLost = gamesLost + 1 WHERE gameName = ?';
}
// update row, then get players new cards
await query(qString, [deckId, req.params.gameName]);
await drawCardToHand(deckId, 'player');
await drawCardToHand(deckId, 'dealer');
await drawCardToHand(deckId, 'player');
await drawCardToHand(deckId, 'dealer');
// get player piles
const playerPileData = await getPileData(deckId, 'player');
const dealerPileData = await getPileData(deckId, 'dealer');
// get game data from DB
qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = ?';
const response = await query(qString, [req.params.gameName]);
// return game data to client
res.json({
gameName: req.params.gameName,
deckId: deckId,
gamesWon: response[0].gamesWon,
gamesLost: response[0].gamesLost,
dealerPile: dealerPileData,
playerPile: playerPileData,
scores: {}
});
});
Finally, we're going to define a few helper functions we used in the previous three routes.
// make api call to draw a card to a specific hand
// note- hand should either be 'dealer' or 'player'
async function drawCardToHand(deckId, hand) {
// draw card from deck
const drawResponse = (await axios.get(`${config.apiEndpoint}/${deckId}/draw/?count=1`)).data;
// place card in appropriate hand
await axios.get(`${config.apiEndpoint}/${deckId}/pile/${hand}/add/?cards=${drawResponse.cards[0].code}`);
return;
}
// make api call to get specified hand data
async function getPileData(deckId, hand) {
const res = (await axios.get(`${config.apiEndpoint}/${deckId}/pile/${hand}/list`)).data;
return res.piles[hand];
}
That's our API built! Now all that's left is the code to start our server.
// run our express server
app.listen(config.PORT, () => {
console.log(`Server running on port ${config.PORT}`);
});
Python API Server (Ignore if you set up the NodeJS API Server
This section will be dedicated to developing our blackjack client API. The purpose of this API is threefold: to interface between the client and requests for web resources (linked stylesheets/scripts, serving webpages and static assets), interfacing between the client and the AWS RDS instance, and interfacing between the client and the deckofcards API.
The first thing you'll need to do is install packages to assist in development. Open up a terminal in the root directory of your project and execute
pip install bottle mysql-connector-python requests
These are needed for the following:
- requests, for making API calls from our server
- mysql-connector-python, for making MySQL queries against a database
- bottle, for intercepting and handling requests to our server
All four of these packages are well reputed, well maintained, and well documented, so you should have no trouble finding resolutions to any issues you have with them.
Now that we have configured our node project, we will assemble our server.py code.
For the following sections, we will be diving into server-side python. If any of the syntax is unfamiliar to you, please check out the or look up any of the relevant syntax to help clarify what's going on.
Add the following lines to import the required packages to our server:
# import appropriate modules
# bottle is the module which will manage our API routes/requests
from bottle import route, run, static_file
# requests handles external API calls
import requests
# mysql connector allows us to connect to a mysql database
import mysql.connector
# allows us to utilize our config file
import json
# load our config json file into a python dictionary
config = json.loads(open('config.json').read())
Next we'll set up our database connection:
# establish mysql connection
db = mysql.connector.connect(
host=config['mysqlHost'],
user=config['mysqlUser'],
passwd=config['mysqlPassword'],
database=config['mysqlDatabase']
)
print('Connection established')
# the cursor is used to query the database
cursor = db.cursor()
Than's all the setup we need! Now we can dive into our API routes.
An API is just a collection of routes and functions used to handle those routes. Essentially, we tell our server that when someone makes a request to www.myurl.com/someEndpoint we want to execute function x, and when someone makes a request to www.myurl.com/anotherEndpoint we want to execute function y.
As you may recall, the first purpose of our API server is to send files to the client, so we will set up two routes for that purpose: the first is so that when someone navigates to our index url (www.myurl.com/) we serve our index.html file, and the second is to serve all files in the public folder (this is mainly to support HTML link attributes).
# bottle routes follow this syntax:
# @route is a decorator, saying the following function should be called
# when we make a request to the specified string.
# this route matches the index request (i.e. www.google.com/)
@route('/')
def index():
# serve a static file, which is hosted in the subdirectory public
return static_file('index.html', root='public')
# you can add a parameter to the path with <PARAM>
# so this route will match yourUrl/exampleRoute, yourUrl/anotherRoute
# but it will not match with yourUrl/exampleIsBad/becauseHasTwoParams
# note the name of the parameter is then passed into the handler function
# this route sends our linked js/css files to the client
@route('/<filename>')
def static(filename):
return static_file(filename, root='public')
That's all there is to setting up our file server.
Next we're going to develop a route that will handle getting or creating a new game. This will get a little less clean, since we have to deal with both the MySQL database and the deckofcards API.
# route to match requests to get an existing or create a new blackjack game
@route('/game/<gameName>/getOrCreate')
def getOrCreate(gameName):
# create our sql query, using %s for query parameters
qString = "SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = %s"
# values is our parameter tuple. we use a comma to force values to be a tuple even though we only have one parameter
values = (gameName, )
# execute the query with parameters
cursor.execute(qString, values)
# capture the result of the query
result = cursor.fetchall()
# no items in result means we need to make a new game in the API and new mysql entry
if len(result) == 0:
# make an API call to request a new deck
res = requests.get('{}/new/shuffle/?deck_count=1'.format(config['apiEndpoint']))
# convert the request body to a python dictionary
data = res.json()
qString = 'INSERT INTO games (gameName, deckId) VALUES (%s, %s)'
values = (gameName, data['deck_id'])
# insert the record of our new game series to the database
cursor.execute(qString, values)
# we must commit so that database insertions are carried out
db.commit()
# draw cards
drawCardToHand(data['deck_id'], 'player')
drawCardToHand(data['deck_id'], 'dealer')
drawCardToHand(data['deck_id'], 'player')
drawCardToHand(data['deck_id'], 'dealer')
# figure out who has which cards
playerPileData = getPileData(data['deck_id'], 'player')
dealerPileData = getPileData(data['deck_id'], 'dealer')
# return game configuration to the client
# note scores are empty since they are computed by the client
return {
"gameName": gameName,
"deckId": data['deck_id'],
"gamesWon": 0,
"gamesLost": 0,
"dealerPile": dealerPileData,
"playerPile": playerPileData,
"scores": {}
}
else:
# game already exists in the database, so load it
# note that fetchall returns a list of tuples, so we must use numeric access
deck_id = result[0][1]
playerPileData = getPileData(deck_id, 'player')
dealerPileData = getPileData(deck_id, 'dealer')
return {
"gameName": gameName,
"deckId": deck_id,
"gamesWon": result[0][2],
"gamesLost": result[0][3],
"dealerPile": dealerPileData,
"playerPile": playerPileData,
"scores": {}
}
Make sure to read through this function and thoroughly understand what it does- if you have questions on the syntax, API response schema, or how the database query process is, read up on the documentation before moving on.
Just two routes left: one to draw a card to a hand, and one to end a game. These routes should make sense to you if you understand what's going on in the previous route.
# respond to request to draw a card
@route('/game/<deckId>/draw/<player>')
def draw(deckId, player):
# draw the card
drawCardToHand(deckId, player)
# return the appropriate player pile
return getPileData(deckId, player)
# respond to the end of a game
@route('/game/<gameName>/endGame/<winner>')
def endGame(gameName, winner):
# get new deck
res = requests.get('{}/new/shuffle/?deck_count=1'.format(config['apiEndpoint']))
data = res.json()
# generate query so that appropriate database cells are updated
qString = ''
if winner == 'player':
qString = 'UPDATE games SET deckId = %s, gamesWon = gamesWon + 1 WHERE gameName = %s'
else:
qString = 'UPDATE games SET deckId = %s, gamesLost = gamesLost + 1 WHERE gameName = %s'
values = (data['deck_id'], gameName)
cursor.execute(qString, values)
db.commit()
drawCardToHand(data['deck_id'], 'player')
drawCardToHand(data['deck_id'], 'dealer')
drawCardToHand(data['deck_id'], 'player')
drawCardToHand(data['deck_id'], 'dealer')
playerPileData = getPileData(data['deck_id'], 'player')
dealerPileData = getPileData(data['deck_id'], 'dealer')
# get updated game data from DB
qString = 'SELECT gameName, deckId, gamesWon, gamesLost FROM games WHERE gameName = %s'
values = (gameName, )
cursor.execute(qString, values)
# fetchone is like fetchall but returns a single tuple
result = cursor.fetchone()
return {
"gameName": gameName,
"deckId": data['deck_id'],
"gamesWon": result[2],
"gamesLost": result[3],
"dealerPile": dealerPileData,
"playerPile": playerPileData,
"scores": {}
}
Finally, we're going to define a few helper functions we used in the previous three routes:
# helper function to draw a card to the appropriate hand
def drawCardToHand(deckId, hand):
# draw card from deck
res = requests.get('{}/{}/draw/?count=1'.format(config['apiEndpoint'],deckId))
data = res.json()
# move card to appropriate pile (hand)
requests.get('{}/{}/pile/{}/add/?cards={}'.format(config['apiEndpoint'],deckId, hand, data['cards'][0]['code']))
return
# helper function to get specified hand data
def getPileData(deckId, hand):
res = requests.get('{}/{}/pile/{}/list'.format(config['apiEndpoint'], deckId, hand))
data = res.json()
return data['piles'][hand]
That's our API built! Now all that's left is the code to start our server.
# run the bottle API server
run(host='localhost', port=config['PORT'])
Client Webpage
If you know nothing about HTML and CSS, I highly recommend you look for a decent primer on those and then come back (w3 schools is a great resource).
We've already developed a boilerplate HTML page and a very basic CSS stylesheet to go along with it for this project, so simply copy the contents into their respective files.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<!--Standard HTML setup stuff, don't worry about what this does, just include it-->
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--Title of the page- shows up in the browser tab-->
<title>Blackjack</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<!--Login information- hidden while in game-->
<div id="login-wrapper">
<div class="input-label">Deck Name</div>
<input type="text" placeholder="myBlackjackDeck" id="deckNameInput" />
<div id="enter-game-button" class="button">Create/Load Game</div>
</div>
<!--Game information- hidden while logging in-->
<div id="game-wrapper" class="hidden">
<div id="dealer-and-player-wrapper">
<div id="dealer-wrapper">
<div class="title">Dealer Hand</div>
<div id="dealer-card-wrapper">
<div class="card">Placeholder Card</div>
</div>
</div>
<div id="player-wrapper">
<div class="title">Player Hand</div>
<div id="player-card-wrapper">
<div class="card">Placeholder Card</div>
</div>
</div>
</div>
<div id="actions-wrapper">
<div id="hit-button">Hit</div>
<div id="stand-button">Stand</div>
</div>
<div id="info-banner">Games won: 0, Games lost: 0</div>
</div>
</body>
<!--Load scripts and styles at the end- ensures elements are loaded before trying to add event listeners, and speeds up l=page load times-->
<link rel="stylesheet" type="text/css" media="screen" href="index.css" />
<script src="index.js"></script>
</html>
/* index.css */
.input-label {
text-align: center;
font-size: 20px;
margin: 20px;
}
input[type="text"] {
margin: 20px;
/*
Calc is a special css property-
it allows dynamic calcualtion of a property.
Used here to ensure input is centered in screen after accounting for margin & border
*/
width: calc(100% - 44px);
text-align: center;
font-size: 24px;
}
.hidden, #game-wrapper.hidden {
display: none;
}
#dealer-and-player-wrapper {
display: flex;
flex-direction: column;
}
#player-wrapper, #dealer-wrapper {
display: flex;
flex-direction: column;
}
#actions-banner {
display: flex;
flex-direction: row;
justify-content: space-around;
}
#actions-banner > div {
margin: 20px 30px 20px 30px;
}
.title {
color: rgb(32, 32, 32);
font-size: 24px;
}
.subheading {
color: rgb(32, 32, 32);
font-size: 20px;
}
.card {
background-color: rgb(137, 137, 255);
padding: 20px;
border-radius: 10px;
text-align: center;
color: rgb(32, 32, 32);
font-size: 18px;
margin: 15px;
}
#hit-button, #stand-button, #enter-game-button {
background-color: rgb(255, 90, 68);
border-radius: 5px;
text-align: center;
font-size: 24px;
padding: 10px;
margin: 10px 0px 0px 0px;
color: rgb(32,32,32);
cursor: pointer;
}
#hit-button:hover, #stand-button:hover, #enter-game-button:hover {
background-color: rgb(190, 61, 44);
border-radius: 5px;
cursor: pointer;
}
Client Script
This section will be dedicated to constructing the client script for our application. The client script has three main responsibilities: maintaining data related to application instance state, updating the DOM (the browser window), and interacting with our client API. All of the following modifications in this section will be made to our index.js file.
The first thing we'll do is set up our event listeners and global constants:
// event listeners, makes our HTML Div elements respond to click
document.getElementById('enter-game-button').addEventListener('click', () => createOrJoinGame());
document.getElementById('hit-button').addEventListener('click', () => hit());
document.getElementById('stand-button').addEventListener('click', () => stand());
// use const instead of magic strings when possible
const DEALER = 'dealer';
const PLAYER = 'player';
// initialize global game variable
// note that after initialization, the game data will have the following relevant properties:
/*
{
gamesWon: number,
gamesLost: number,
gameId: string,
gameName: string,
scores: {
player: number,
dealer: number
},
playerPile: {
cards: [
{
value: 0-9 || JACK || QUEEN || KING || ACE,
suit: string,
}
]
},
dealerPile: object with same format as playerPile
}
*/
let game = {};
There's not much to talk about here, so we'll move on to defining the three functions we want to use to handle our three event lsiteners:
// response to player hitting 'enter-game-button'
// requests game for a given game name from our API server
// note that this is an async functions. This means that within this function, we can use the keyword 'await'.
// this allows us to minimize the number of callbacks we have to use
async function createOrJoinGame() {
// get deck name from input
const gameName = document.getElementById('deckNameInput').value;
// make API call to server to either continue an existing game or create a new game
const gameData = await fetch(`/game/${gameName}/getOrCreate`);
// get JSON body of API response
game = await gameData.json();
// hide the login details, show the game details
document.getElementById('login-wrapper').classList.add('hidden');
document.getElementById('game-wrapper').classList.remove('hidden');
// begin the game
initGame();
};
// response to player clicking 'hit-button'
// gets card from API server, determines if player has lost based on newly drawn card
async function hit() {
// draw card from API
const playerPile = await fetch(`/game/${game.deckId}/draw/player`);
// update player pile with the pile retrieved from the API
// note that playerPile.json() is an async function, so we have to await it before asking for .piles.player
game.playerPile = await playerPile.json();
// update display with new card
updateGameDisplay();
// recalculate score
game.scores[PLAYER] = scorePlayer(PLAYER);
// use setTimeout to allow browser to re-draw cards before checking for a loss
// don't worry about this workaround, it's probably beyond the scope of what you need to care about
setTimeout(() => {
// determine if player automatically wins or loses
if (game.scores[PLAYER] > 21) {
alert(`You lose, score = ` + game.scores[PLAYER]);
endGame(DEALER);
} else if (game.scores[PLAYER] == 21) {
alert('You win, score = 21');
endGame(PLAYER);
}
}, 1);
};
// response to player clicking 'stand-button'
// causes dealer to draw cards until satisfied, then calculates a winner
async function stand() {
// most basic AI possible- dealer should draw if they have less than 17 points
while(game.scores[DEALER] < 17) {
// request new card for dealer from API
const dealerPile = await fetch(`/game/${game.deckId}/draw/dealer`);
// update dealer pile
game.dealerPile = await dealerPile.json();
// update dealer score
game.scores[DEALER] = scorePlayer(DEALER);
updateGameDisplay();
}
// determine game winner
setTimeout(() => {
if (game.scores[DEALER] > 21 || game.scores[DEALER] < game.scores[PLAYER]) {
alert(`You win. ${game.scores[PLAYER]} to ${game.scores[DEALER]}`);
endGame(PLAYER);
} else {
alert(`You lose. ${game.scores[PLAYER]} to ${game.scores[DEALER]}`);
endGame(DEALER);
}
}, 1);
};
Here you'll see interaction with our API: each fetch will make a GET request to our client API- you should note that the route specified in each fetch request matches with a route we've set up on our server.
Next we'll define the helper functions that we use in our three event responders
// calcualtes player score
function scorePlayer(player) {
// determine if we are claculating a score for the player or the dealer
const pile = player == PLAYER ? game.playerPile.cards : game.dealerPile.cards;
let score = 0;
// count aces, since they can be 1's or 11's
let aceCount = 0;
// fancy for loop, basically passes card in instead of needing to use pile[i] like a traditional for loop
pile.forEach((card) => {
// if the card value is numeric, we can directly add it
if (!isNaN(card.value)) {
// API represents 10s as 0s, so account for that in our addition
score += card.value == 0 ? 10 : parseInt(card.value);
// deal with face cards
} else if (card.value == 'KING' || card.value == 'QUEEN' || card.value =='JACK') {
score += 10;
// otherwise we've got an ace- don't forget to count it.
// we assume we want to count an ace as an 11
} else {
aceCount += 1;
score += 11;
}
});
// if we have aces and our score is too high, we want to count them as 1s instead of 11s
while (score > 21 && aceCount > 0) {
score -= 10;
aceCount -= 1;
}
return score;
}
// finished with our game
async function endGame(gameWinner) {
clearGameDisplay();
// make API call to get a new deck and new starting draw
const gameData = await fetch(`/game/${game.gameName}/endGame/${gameWinner}`);
game = await gameData.json();
initGame();
}
// set up scores/display after getting a new game object
function initGame() {
game.scores[PLAYER] = scorePlayer(PLAYER);
game.scores[DEALER] = scorePlayer(DEALER);
updateGameDisplay();
}
// update visual components of the game
function updateGameDisplay() {
// grab our card wrapper elements
const pCardWrapper = document.getElementById('player-card-wrapper');
const dCardWrapper = document.getElementById('dealer-card-wrapper');
// remove all cards from them
pCardWrapper.innerHTML = '';
dCardWrapper.innerHTML = '';
// give dealer their cards
// draw new div elements for each card
game.dealerPile.cards.forEach((c) => {
// create div element
const card = document.createElement('div');
// give div a class 'card'
card.classList.add('card');
// give the card its text content, accounting for the 0 represented as a 10 thing
card.innerHTML = c.value == 0 ? '10 of ' + c.suit : c.value + ' of ' + c.suit;
// add the card to the appropriate wrapper element
dCardWrapper.appendChild(card);
});
// give player their cards
game.playerPile.cards.forEach((c) => {
const card = document.createElement('div');
card.classList.add('card');
card.innerHTML = c.value == 0 ? '10 of ' + c.suit : c.value + ' of ' + c.suit;
pCardWrapper.appendChild(card);
});
// update the score information
document.getElementById('info-banner').innerHTML = `Games Won: ${game.gamesWon}, Games Lost: ${game.gamesLost}`;
}
// clears cards while loading the next game, to minimize confusion
function clearGameDisplay() {
document.getElementById('player-card-wrapper').innerHTML = '';
document.getElementById('dealer-card-wrapper').innerHTML = '';
}
And that's all there is to it! Take some time to digest how this whole front-end application works, digest the syntax, look up things you don't understand.
Testing and moving to server
Now that all your code works, all that's left is to test it and get it running on a server.
To run your code, in a terminal cd to your project root and execute
npm start
You should see Server running on port 4000 printed to the terminal. Now if you navigate to localhost:4000/ in a browser window you should be able to play your blackjack game!
Test to make sure everything is working properly, and then get all your code up to git.
To get your code running on a server, follow the directions at this tutorial.
Authors
Ethan Shry, Winter 2018
Group Link
N/A
External References
N/A