[MUSIC PLAYING]
BRIAN YU: OK, let's resume.
Welcome back to CS50 Beyond, everyone.
Hope you had a good lunch.
So just as a recap, this morning, we talked
a little bit about Python and Flask did a review of some
of the features of Python, looked at some new things
that we didn't explore in CS50, things like the input function,
things like exception handling and how to handle
them, diving a little more in detail into data
structures, like dictionaries and sets, and such.
And then we took a look at Python, or Flask,
looking at building web applications with variable routing
and looking at how we can use that to begin to build more interesting, more
sophisticated web applications.
This afternoon, we're going to pick up where we left off,
dive a little bit more into Python, looking at some other language
features, looking at recursion, looking at object-oriented programming.
We'll take a look at how to take a Flask web application, which is right now
just running locally on your computer or in CS50 sandbox,
or elsewhere, and actually deploy it to the internet.
We'll use Heroku, a service where, for free, you can take a web application
and deploy it to the internet so that anyone can use it from anywhere.
And finally, we'll conclude by taking a look at another domain within computer
science, that of artificial intelligence,
looking at how to write programs that can make computers intelligent,
and, in this case, intelligently play games,
although the algorithm that we're going to use today,
called Minimax, to intelligently play a game
is an algorithm that can be applied to situations.
More generally, whenever you need a computer to be able to make decisions
in situations where different decisions can ultimately
lead to different outcomes.
So that's going to be the roadmap for where we're going this afternoon.
But before we do that, what questions do you have?
OK, with that, we'll go ahead and take one look at a Flask feature
that we didn't quite touch on, but that is going to be ultimately useful.
I've prepared a simple Flask application right here that has two routes,
a slash route-- that's just a default indexed route--
and a slash more route that ends up redirecting us to a more.html page.
If I open up index.html, it's a pretty simple website that just says hello.
The background has been styled to be blue via the style body background
color blue there.
And inside more.html, I've done, basically, the same thing.
The only difference is that there's some text inside the body of the page that
wasn't there in index.html.
And so this is a situation that came up in CS50,
and I just want to remind you of this, where
it can often be helpful to do what's called template inheritance.
That more.html and index.html share a lot of the same code.
Almost all of it is the same.
The only difference is what's inside the body of the page.
And so this can be a common case where it
would be useful to factor out the common code into a file that's just
called layout.html, for instance, and then
reference that file in the index.html and more.html.
So I demonstrate that very briefly just so you get a refresher on that,
so as you build Flask applications, it's something at your disposal as well.
So I'll create a new file called layout.html,
and it's going to look basically the same as what the index page looks like.
But the area where my pages are different-- the header is
the same, the title is going to be the same,
the styling is going to be the same.
The only difference is in the body of the web page.
And here, I just want some custom block, whereby in index.html and more.html,
I can add code into that blocks.
So I can say, OK, inside this block, inside the body, I'm going to say,
create a block that I'm just going to call a body.
But it could be called anything.
It could be called main, or content, or some other name for the block.
And then end block, again, using the curly braces and percent signs.
It's typical [INAUDIBLE] syntax.
And all I'm doing here is basically saying, inside the body of this layout,
that HTML page, is a section where I'm going
to insert code at some point in time.
And so that means I can very easily make some modifications to index.html
to simplify this page dramatically.
I can get rid of, basically, all of the code,
except for hello, which I'm going to need.
And I'll just say at the top, extends layout.html, meaning take layout.html
and use that as the starting point for this HTML page.
And the only thing that I need to change is to say, OK, inside of block body,
instead of it being empty, let me just put the word hello there.
And likewise, in more.html, I can do the same thing, extend layout.html.
And inside of block body, just say more text, or whatever it might be.
I've simplified both index.html and more.html
dramatically because all of the common code is just inside of layout.html.
So now, if I run this Flask application by typing flask run,
open up a web browser, you see hello with the blue background.
I go to slash more, you see the same thing, also with the blue background.
And that common code of the title, the blue background, all of that
is there because of that shared template inheritance.
So just wanted to demonstrate that so you had an opportunity to see
that in practice.
Questions about template inheritance before we move on?
Yeah?
AUDIENCE: When you wanted to change the style of more, for instance,
but then the style is going to be [INAUDIBLE]..
So how would you do that?
BRIAN YU: Great question.
So the question is, all right, in layout.html, we've put some styling.
We made the background color blue.
What if I wanted to add other styling to this page?
Well, what you could do is just add another block.
Add a block, which I'll call block style, for instance,
and end block there.
And you could then, inside of index.html or more.html, add block style.
And maybe for consistency, I'll put block style up at the top.
And you could add some code here.
You could add, OK, I want the body to have a font size of 24 pixels,
or something like that, whereby now, if I run this web application
and go to slash more, more text is larger.
If I go to slash more, notice the size of the text gets bigger.
So I've been able to add style code just by putting it in block style,
and block style is just going to be inserted right here inside of the web
page.
So you can add as many blocks as you'd like in order
to customize your layouts however best fits your needs in the program.
Other things?
OK, so there are two Python-related, or programming concepts,
more generally, that I wanted to talk about today before we move on,
the first of which is recursion.
So this is something that we touched on briefly.
If you took CS50 this past semester, it came up briefly on the CS50 quiz.
But recursion is this idea of we can have functions,
functions that are running code.
But functions can also call themselves.
We know that functions can call other functions,
but functions can call themselves, oftentimes,
to create some sort of useful effect.
And so the function that I'm going to write here
is a function that's going to calculate-- this the classic, one
of the classic examples of recursion-- calculating the factorial of a number.
So the factorial, I'll just do some comments here.
I'll do factorial.pi.
If I had a number like 5 factorial, 5 factorial is equal to 5 times 4 times
3 times 2 times 1, which is equal to 120.
If I add something like 4 factorial, that's 4 times 3 times 2 times
1, which is equal to 24.
So a number factorial is just that number
times the number one less than it times the number one less than it,
so on and so forth, up until you get to 1.
You multiply that all out, and the result
is whatever you get as the value of the factorial.
And I would like to write a function that
calculates the factorial of some number, some number n, for example.
So I define a function called factorial n.
And the interesting thing that I can do here
is notice that 5 factorial is really just 5 times 4 times 3 times 2 times 1.
And this 4 times 3 times 2 times 1 is the same as just 4 factorial.
And so the recursive idea that I can use here is the factorial of some value n
is going to be just n times the factorial of n minus 1.
I take the number and multiply it by the factorial of the number
one less than it.
So 5 factorial becomes five times the factorial of 4.
And 4 is just 4 times the factorial of 3.
Now, what goes wrong if I try and use this code?
Something's not quite right about it, although the idea is reasonable.
Yeah?
AUDIENCE: It breaks at one or minus one.
BRIAN YU: Yeah.
So it breaks, when I get down to lower numbers, like factorial of 1.
Because then I'll do 1 times factorial of 0, and factorial of 0,
by this logic, would be 0 times factorial of negative 1.
And we're just going to keep going into the negative numbers now
and it's just never going to stop.
So when I write a recursive function, it's
important to have what's called a base case, some point at which this function
is going to stop running.
And so I could say something like, OK, 1 factorial is just 1.
So the simple way to do this-- as there are multiple ways to do this--
is to say, all right, if n equals 1, then just return 1.
Because the factorial of 1 is just 1.
And so I could do something now and say, all right,
I'll open up a Python interpreter from factorial.
Import the factorial function.
And I can say, OK, what's the factorial of 1?
And it is, in fact, 1.
And I can say, OK, what's the factorial of 2?
Well, that'll be 2 times the factorial of 1, which is 2.
Factorial of 3, factorial of 4, factorial of 5,
and we're getting the correct results just by calling the function itself
again, by recursively calling the function on a smaller and smaller sized
problem until we get down to a problem the size 1, and then we've handled it.
Now, this isn't quite perfect because it doesn't handle factorial of 0,
but you can make some slight modifications
to make it handle that as well.
But that's the general idea of recursion.
And recursion is actually going to come up later this afternoon,
as we start to talk about artificial intelligence,
but it comes up more generally in programming in many different cases.
And in fact, if you go on to take CS51, recursion
is going to be a big component of that course
and thinking recursively and thinking about how to write recursive functions.
So just wanted to introduce that.
Yes?
AUDIENCE: Can you show what it looks like if you forgot the base case
but try to run it?
BRIAN YU: Great question.
What happens if you forget the base case but try to run it anyways?
We'll give that a shot.
So if I didn't include if n equals 1 return 1,
and I just had return n times factorial of n minus 1,
I'll load up Python again.
Say from factorial import factorial.
This is that idea of importing a function from a module
that we talked about earlier this morning.
If I try and do factorial of 5 right now,
for instance, I'm going to get an error.
I get a recursion error, specifically which
is that maximum recursion depth has been exceeded.
I've tried to call a function on its own too many times.
Python lets you call a recursive function up to 1,000 times,
and I have exceeded that because this would go on infinitely
if it would let me do so.
So that is what happens if you don't have a base case.
If you ever see a recursion error, which you might this afternoon if you choose
to implement the artificial intelligence algorithm,
that's what you can expect to see.
Questions about the idea of recursion?
Yeah?
AUDIENCE: With the line return at times factorial minus 1,
is that factorial function already predefined?
BRIAN YU: This factorial function is not predefined.
This factorial function is just a name that
is referencing the function itself.
So this function, as part of the way it works,
is going to use itself to try and calculate
the answer to a smaller problem.
AUDIENCE: So you don't really need the thing in purple, right?
The top line will still do the job?
BRIAN YU: You don't need the thing in--
AUDIENCE: The last three lines, you don't really need them, right?
BRIAN YU: So you do need these three lines,
and the reason is this is handling the logic of what the function is doing.
So the first thing my function is doing is checking
to see if I've hit my base case.
If I'm calculating the factorial of 1, I'm
just going to pre-program into my function the answer to that is 1.
But if I'm trying to calculate the factorial of anything else--
presumably something larger than 1-- but you should probably
add checks to handle things less than that,
then to calculate the factorial of 5, for example,
I'm going to take the number 5 and multiply it
by whatever I would get by taking the factorial of 4 of n minus 1.
AUDIENCE: How does the computer know what that factorial n minus 1 is?
BRIAN YU: Good question.
How does the computer know what the factorial of n minus 1 is?
It calls the function again.
So now we're running factorial one more time.
We're calculating factorial of 4.
That's not one 1.
We're going to return 4 times the factorial of 3.
So now, we're calculating the factorial of 3,
which is 3 times the factorial of 2, which is 2 times the factorial of 1.
and the factorial of 1, as these if conditions on lines 9 and 6
will indicate, is just going to be 1.
So it's a slightly different way of thinking.
It can be confusing to reason about it first.
It seems like I'm assuming that the problem is solved
and just using that in order to solve the problem, which doesn't quite
make sense sometimes.
But if you try it on smaller examples, you can sometimes get a sense for it.
And hopefully, when we get to the algorithm
that we'll show later this afternoon, that
will help it make more sense as well.
Other things?
OK, so that's recursive programming.
And one other programming model or paradigm that I thought
was probably worth demonstrating, or at least showing, is
the idea of object-oriented programming, something
that you might see in other languages, as well, but that Python supports,
and something that will come in handy for us
as we begin to deal with databases and dealing with web applications that
interact with data later on.
And the idea of object-oriented programming
is that object-oriented programming is a programming paradigm that's
based on the idea of an object.
And all an object is just some structure in our code
that's a combination of data that we're storing about the object,
and also some code that we can run with the object.
So it's a way of encapsulating data and code together inside
of one common unified structure.
And so I'll give a simple example of this.
Maybe I want to define a way of keeping track of coordinate points,
so x and y-coordinate pairs.
You could imagine that I could just say, OK,
x equals some value and y equals some value,
and then just manipulate those x and y values together.
But it would be nice to be able to take those two values
and encapsulate them inside of a single structure.
Now, we saw that we could use a tuple to do this.
I could say coordinates equals x comma y.
Coordinates equals x comma y.
And now coordinates is equal to 28 comma 50.
So you can do that to encapsulate the structure together.
But as things get more complicated as there
becomes more and more pieces of data that you like to associate with this.
Maybe not just two fields, but 10, or dozens of fields.
And as you want to begin to perform computations and operations
on these coordinates, this sort of programming
can sometimes start to get a bit difficult.
And so object-oriented programming is designed
to make this a little bit easier.
And so I'll create a new file called points.pi.
And I'm going to define a new class called point.
And you can think of a class as just a new type of object
that I'm allowing myself to create.
So the string, for instance, is a class that already exists
and we've been able to use and manipulate.
The list, for instance, is another example of a class, some type of thing
that I can manipulate.
And I'm creating a new class called point that is just
going to store x and y-coordinates.
And so now, what I'm going to write is what's called a constructor function.
This will look a little bit cryptic at first,
but hopefully it'll become clear in just a moment.
The constructor function in Python, by default, is called init, __init__,
for initialize.
And __init__ is going to take three arguments-- we'll see why in a moment--
self, x, and y.
Self is going to refer to the object in question
and x and y are going to be arguments that we're
going to provide when we first create this point.
And so I'll show you what this function is going to look like.
To create the point, I want to update the object.
Self is the name of this object.
And I'm going to set a property of it called x to be equal to whatever
the argument is, just equal to x.
And I'm going to say self.y equals y.
So this looks a little bit strange, probably unfamiliar
if you haven't seen this sort of syntax before.
But what I've done is I've created a class now
that allows me to just create point objects.
And so to create a point object, what I'm going to do is say p--
just some variable--
equals point.
That's the name of the class.
And then I'm going to provide the x and y arguments to it.
I'm going to say point--
the x-coordinate will be 10 and the y-coordinate will be 20.
And so just like that, I've been able to create a point object,
passing in 10 as the value of x and 20 as the value of y.
And I don't provide anything for self.
Self is just this assumed argument in the world
of Python object-oriented programming that just refers to the point
that I have just created.
So now, if I wanted to print out the x-coordinate of this point,
I could print out p.x.
In Python objects, you use the dot notation, something dot something else,
to access a particular property of that object.
And I can print out p.y to print out the y property of this particular object.
And so now if I run this program, by running Python points.pi, what I get
is I get printed out 10 and then 20.
Because it printed out the x value of the point
and then it printed out the y value of the point, and both of them
are stored inside of this object that's just called p.
So this is a very new way of thinking about programming,
a different way of thinking about data, a different way of thinking
about how to organize information inside of our objects.
Questions about any of this?
Very new.
Yeah?
AUDIENCE: Were any visualization functions gaps for self?
Or is there any alternative that you can write for self?
BRIAN YU: Good question.
The question is, does this need to be self?
I think, technically, it could be something different,
but the convention in Python is to call it self.
So generally best to call it by its convention.
Other things?
Questions about this?
Yeah?
AUDIENCE: What is the __init__?
BRIAN YU: __init__ stands for initialization.
This is basically, how do I create a new point?
And what I'm saying is to create a new point,
I need to provide two arguments, an x and a y-coordinate.
And I set the x value of the point to be whatever
that x argument is, and I set the y value of the point
to be whatever the y argument is.
And I use this function here on line 8.
When I create a new point by saying point 10, 20,
that's calling this __init__ function, passing in the value of x and the value
of y as 10 and 20.
And I can show you this.
If I add a print statement here, I'll say creating a new point
with coordinates x and y.
So anytime I run this __init__ function, it's going to say creating the new
point with coordinates x and y, such that now if I run this code,
it says creating a new point with coordinates 10 and 20.
Because 10 and 20 were passed in as the arguments to that __init__ function,
and then it creates the point so that I can then print out what the values of x
and y actually are.
Yeah?
AUDIENCE: instead of saying "self," could you say "he" [INAUDIBLE]??
BRIAN YU: Yes.
So someone else asked this a moment ago.
Self is just the convention in Python.
It's generally accepted as a way of referring to the object in question.
So when I say self.x, I am modifying self, the object,
and updating it to store some additional data alongside it.
And so there are other things I can do here.
I can add another function, or what's called
a method, in the world of object-oriented programming called
shift, for example.
If I wanted to shift by a certain x and y amount, like move the point
to a different location, then I would say,
OK, self.x equals self.x x plus however much
I want to shift the x-coordinate by.
And self.y equals self.y plus however much
I want to shift the y-coordinate by.
And so now, if I were to say something like p.shift,
shift two in the x direction and three in the y direction,
and then print out x and y.
Well, then what I'm going to get is I'm going to get 12 and 23.
So you look at that code to see why it works.
I'm calling the shift method, which is just
a fancy word for function in the world of object-oriented programming,
passing in 2 and 3 as the arguments to that, x and y.
And then I'm updating self.x and self.y, the values stored inside of the object,
to increment themselves by however much I want to shift the point by.
So this is the basic idea of object-oriented programming.
We're not going to use this too much today,
but we'll come back to it in about two days time.
I just wanted to introduce it now so that when we see it in two days' time,
it looks a little bit familiar.
And we'll see how this become very, very practical when
we start dealing with databases and dealing with SQL
and having our Python code interact with a SQL database
without the help of a CS50 SQL library, for example.
Questions about that before we move on?
Yeah?
AUDIENCE: Could you have built a keyboard from the Tic-Tac-Toe thing
as [INAUDIBLE]?
BRIAN YU: Great question.
Could you have designed the Tic-Tac-Toe game board using a class
instead of using the list of lists that we were using this morning?
Absolutely.
And potentially, that might be a better design for it
as you think about the different operations you
might want to do with it.
But yes.
And you're welcome to try that, too, if you'd like to, later this afternoon,
if you want to try and use an object in order to do that.
Yeah?
AUDIENCE: So because we created the class there,
do you need to use the self documentation for future functions
that you're going to write?
BRIAN YU: Yeah.
Any functions that are inside of this class, or what we'll call methods,
like the shift function, they all need to take the self argument if they're
going to operate on a specific point.
Because the idea here is I can have multiple points.
I could create another point called P2 and that
could have different coordinates.
And that point has its own x value and its own y value,
and those are manipulated separately.
And so self.x is a way of saying this object's x property, or this object's y
property, as opposed to some global x and y property that all
of the objects share.
They're specific to a particular object.
Other things?
All right.
So we'll wrap up there for Python, but I thought what we'd do now
is take a look at how to actually take a web application
and deploy it to the internet.
Right now, we've been running Flask applications on our own computers
locally by typing flask run, and as soon as we quit Flask,
the application stops running.
It stops working.
What we'd like to do with being able to host our Flask web applications online
on the internet, such that anyone can access them, should they desire to.
And there are a number of different ways to do this, but one of the easiest
happens to be a service called Heroku.
Heroku offers a bunch of different tiers,
but there is a free tier that allows you to upload and run
a fast web application online for free.
And that's the one we're going to be exploring today.
So a couple of steps.
What I'm going to do is I'm going to try and take the Tic-Tac-Toe example from
earlier this morning.
And this is one that's not quite working yet.
Yeah, this version doesn't actually let you play,
but we'll try and at least upload it so you can see the board online.
And we're going to take this Flask application
and we're going to prepare it for Heroku and then deploy it to Heroku.
And so I'll show you the steps for doing that.
The steps are also online on CS50's documentation.
And of course, this lecture video will also
be online later, too, if you want to go back and reference that later.
So the first thing I'm going to do is Heroku
requires a file called the Procfile that basically just site decides,
when I start running this application, what exactly is going to happen.
And so there are a lot of steps that are going to be involved here.
None of it is particularly necessarily intellectually interesting,
but it's good to get an understanding, or at least get exposure
to it, such that if you want to do something like this in the future,
it's available to you.
But certainly don't feel like you need to memorize this or anything.
So inside the Procfile, I'm going to add this line, which
is going to look a little cryptic.
I'm going to say web colon gunicorn application colon app.
OK.
So what on earth is going on with that line?
Web, this is saying, OK, when I'm going to launch this,
I'd like to launch a web server.
And what exactly should I do when I try and launch the web server
for this Heroku application?
Well, I'm going to run gunicorn, which is basically a web server program.
Right now, we've been just using a program called Flask
as our web server, which has the same name as the Python library called
Flask.
gunicorn tends to be a little bit more stable, a little more robust, better
for a production quality program, if you're
trying to deploy something to the internet for people
to use more globally.
Application is the name of the Python file
from which I'm going to be running the application from.
So it's called application.pi.
So I'm going to say application.
And then app is the name of the variable inside of application.pi
that represents my Flask app.
This word "app" corresponds to the word "app,"
here on line 5, where I said, OK, create a flask application and call it app.
That name corresponds to this name in the Procfile.
So this is just instructions to Heroku in a very succinct and sort of obscure
way, saying when you start up this application,
run this server, gunicorn, from an application, dot pi file,
and run the web application called app, basically
telling the server what to do.
The next file that I need is a file called requirements.txt.
requirements.txt is a file that's going to contain dependencies.
In other words, what things does my server on Heroku
need to install in order for my application
to be able to work correctly?
You probably faced similar issues when you
were trying to run web applications earlier this morning, where
you needed to install Flask and you needed
to install Flask-Session in order to get the application working
on your own computer.
Same thing is going to be true of Heroku servers.
So when you try and upload a web application there,
it needs to have the necessary dependencies installed in order
for the application to be able to work.
So what are the requirements?
Well, we need to install Flask, for sure.
We need to install Flask-Session, for sure.
And the third thing that we need, in this case,
is gunicorn, the name of the web server that we're actually
going to be using in order to run this web application.
So all right, I've created a Procfile, created a requirements.txt file.
All of this is just to set up my application such
that I can deploy it to Heroku now.
Questions about what I've done so far?
OK.
Next step, there are a number of ways to deploy something into Heroku.
The easiest way is probably just to deploy it from a git repository.
So what I can do now is go to github.com.
Recall that we learned about git and GitHub yesterday.
I'm going to go ahead and create a new repository.
I'll call it Tic-Tac-Toe, and I'll go in and create the repository.
And now, I'm going to follow a number of different steps here.
It's telling me what commands I need to run in order to set up my repository.
So I need to run git __init__ in my Tic-Tac-Toe directory.
git __init__ basically just sets up this directory, this folder,
as a Git repository that I can then push somewhere.
So all right, I've initialized a new Git repository in my Tic-Tac-Toe directory.
And now what I'd like to do is set it up so that when I push,
it's going to push to this GitHub repository.
And that's this command, git remote add origin, basically just saying,
remember that origin is where on GitHub am I going
to push my code to when I run git push.
So I'm going to copy that command, paste that in there.
I've added the origin, saying I'm going to push to GitHub.
And now, I'm going to add all my code to the Git repository.
I'm going to say git add dot to say, add everything in this current directory.
I'm going to commit these files.
So say, prepare to deploy to Heroku is what I've just done.
And now I'm going to push, git push.
And I'm probably going to get a message that says,
all right, I need to specify where to push to.
I'm going to push to the master branch, just the default
branch of the repository.
So I push that code to my Git repository, so far all things
that we've seen from yesterday.
If I refresh this page, my application.pi, templates directory,
Procfile file, requirements.txt, they're all located now inside
of my Git repository.
And now, I just need to tell them my Heroku account
to launch a web application that's based on this Git repository.
So I'll now go to heroku.com.
And I mentioned an email near the beginning of class,
that you should sign up for Heroku accounts.
You're welcome to do so, if you'd like to,
in order to experiment with this type of thing on your own.
I'm going to go to the upper right, click on New, and say Create New App.
I want to create a brand new web application.
I need to give it a name.
I'll call it brian-tictactoe.
Hopefully, nobody's taken that name.
All right, it's available.
Great.
We'll create that app.
And OK, I've created a new Heroku app.
And now, the next step I need to do is link it to my GitHub repository
so it's actually pulling the code from GitHub.
So under Deployment Method--
again, there are a number of different ways to deploy the application.
The one we're going to explore today is deploying from GitHub.
So I'm going to click on GitHub.
And if you're doing this for the first time,
you probably need to authorize it on GitHub's end, maybe log into GitHub
and click Authorize to approve Heroku talking to GitHub for you.
And then it says, connect to GitHub.
Find a repository.
The name of my repository is Tic-Tac-Toe.
I'll go ahead and search for that.
All right, here's the repository.
I'm going to click Connect, which is going
to link that GitHub repository to this Heroku application
that I'm deploying to the internet.
So I click Connect there.
And great, now it's connected.
And now, if I want to, I also have this thing
called Automatic Deploys, where you can say anytime
someone pushes to the master branch, automatically
redeploy the application so that the version online matches whatever
is on the master branch.
And this can be very powerful because it means that any time you make a change,
you can update the version online just by typing git push,
push into the master branch.
And Heroku will automatically do the deployment.
This also makes it very important that the master branch is a working branch.
And so again, if you're trying features, experimental things,
things that might break, always a good idea to experiment
on a separate branch.
And only when you feel confident about it,
merge it back into master, which will trigger the automatic deploy.
So I'll go ahead and enable Automatic Deploy here.
So anytime I push to master, it will automatically deploy.
You can also manually deploy.
Say, all right, I'd like to deploy this application from the master branch.
And I'll go ahead and click Deploy Branch.
And we'll see if this works.
Hopefully, it works.
You can see it's installing Python.
It's installing pip.
In a moment, it's going to try and install my dependencies, so things
like Flask and Flask-Session, which are the different dependencies I
defined in requirements.txt.
And then it's going to try and start up my application.
Questions about anything while this is running?
I see some green smiley faces.
Great.
All right, it's launched the web application.
It's given me a URL.
If I take this URL, copy it, go to a new page,
this is brian-tictactoe.herokuapp.com.
Press Return, and all right, great.
Tic-Tac-Toe shows up deployed on the internet.
And if you all go to that same URL, you can all see it now as well.
If I make changes to it, push it to the master branch,
it will also deploy online there, too.
So I've been able to successfully deploy a Flask application to the internet
by Heroku.
Questions about that?
Yeah?
AUDIENCE: So does gunicorn just understand the Flask syntax
and treats it as its own?
Because you said it's from Flask or more dependable.
BRIAN YU: Yeah, good question.
So gunicorn is just a separate server application that's
designed to sort of launch web servers and it
understands how to take a Flask application
and actually serve it to handle incoming requests.
Other things?
Julie do you have something?
AUDIENCE: Do you have a particular domain [INAUDIBLE] that you look up?
BRIAN YU: Oh, good question.
So it's about the domain.
If you look up at the top right now, I'm at brian-tictactoe.herokuapp.com.
brian-tictactoe was the name of the app that I gave it
to that just happened to be available.
Herokuapp.com is just the default name for where
these applications are hosted.
But maybe you would like to host your website somewhere else,
at like, briantictactoe.com, for instance, or some domain that you've
purchased that you'd like to connect.
So the way you can do that is by Heroku Settings.
The first thing you'll need to do is actually purchase the domain,
and there are a number of services via which you can do that.
But once you own it, if you go to the Settings of your application,
here in Heroku's control panel, you can just add a domain to the application.
And if you just click that Add Domain button there,
that will allow you to then connect that domain to the web application
on Heroku.
You'll have to do a little bit of configuration
that Heroku's website will guide you through,
but it will then allow you to connect some domain to the application that's
running on Heroku.
Yeah?
AUDIENCE: So if someone typed in brian-tictactoe.herokuapp.com,
even after you connected the domain, will it still be [INAUDIBLE]??
BRIAN YU: Good question.
If someone went back to brian-tictactoe.herokuapp.com,
would it still work?
Yes, it probably would.
Yeah?
AUDIENCE: What's the file extension for Procfile?
BRIAN YU: No file extension for the Procfile.
Just capital P, Procfile.
No extension necessary there.
Other things?
All right, so that's deploying a web application to the internet.
You can do this with Flask applications.
You can do it with other applications as well.
When we go into building React applications later on in the week,
you can do the same thing there.
So as you build any of your applications this week,
if you'd like to give deploying into the internet a try, or even applications
you build after this class or outside of this class, feel free to do so,
and these steps should help you get up and running with that.
Other questions about Heroku and deploying our web application that way?
Yeah?
AUDIENCE: Do you think that Heroku is better than the GitHub for hosting?
BRIAN YU: Oh, good question.
So the question's about GitHub hosting service, GitHub Pages.
So GitHub Pages is used for serving static sites.
For those of you who took CS50 this past semester,
you might remember problem set 5 home page, wherein
you were able to create, basically, an HTML CSS web page for yourself,
about yourself, or something you're interested in.
And then if you wanted to, we gave you an option
to deploy that to the internet.
And we did that by GitHub Pages, which is a service built into GitHub.
It's free.
And it basically lets you take a repository and turn it into a web page
that GitHub will host for free.
That works only for static pages, pages that
are just static HTML CSS JavaScript.
But it doesn't work for if you're trying to run a web server,
so something like the Tic-Tac-Toe game that we have that
has some backend server, where you're making requests
to a server that's processing results and rendering templates.
You can't run a Flask app off of GitHub Pages, for example.
So Heroku is a little bit more flexible, in that sense,
in that you can deploy static sites.
You can also deploy dynamic sites that have the sort of interactivity
that you might want, that have a server that you can talk to,
that have a database, for instance, whereas GitHub Pages can't quite
do that.
Yeah?
AUDIENCE: For the requirements.txt, do you
need to add into that every library that you're using in your Python code?
BRIAN YU: Good question.
So yeah, in requirements.txt, you're going
to need to include in it any library that you
have installed into your Python code.
So if you're using some external library,
you're going to need to include that in requirements.txt.
Otherwise, Heroku's not going to be able to know that it needs to install that
and you're probably going to get a Module Not Found Import Error
type of thing when you try to deploy the application.
And if you try and deploy it and something goes wrong,
which does happen from time to time with Heroku,
you can always go to the logs, which show you
the errors that have happened as you were working
on trying to deploy the application.
And those errors can often help you get guided
to what it is that you need to change or fix to get it working.
All right, other things?
OK, then we'll change gears here a little bit
and move away from web programming, just for a little time,
in order to talk about the world of artificial intelligence.
So we've been making a Tic-Tac-Toe game so far this morning.
So far, you've probably implemented, hopefully,
the ability for a user to make a move, or at least an ability
to update the board based on the move.
Maybe you've gotten around to implementing, checking whether or not
someone's won the game or the ability to reset a game.
But what we'd like to do now is not just allow
humans to play against other humans, but to allow a human
to play against some artificial intelligence that is able to behave
in some sort of intelligent way.
And so what we're going to do this afternoon
is explore one algorithm called Minimax that
is particularly useful for two-player games,
especially, wherein both players are trying to compete for differing
outcomes and players need to each make moves
based on choosing among the available moves that are there.
And so we have here a Tic-Tac-Toe board.
And that Tic-Tac-Toe board we can really divide
into three possible outcomes of what can happen
at the end of a Tic-Tac-Toe game.
Either X wins the game or O wins the game or nobody wins the game.
And so those are the three possible resulting states.
And of course, there are many ways to get to those outcomes.
There are many ways X can win, many ways O can win, many ways nobody can win.
But those are the three possible outcomes.
And what we're going to do is to make this into a computational--
you put this in computational terms, mathematical terms
that a computer can understand.
We're going to assign each of these possible outcomes a score.
So if X wins, we're going to say that board has a score of 1.
And if O wins, we're going to say that board has a score of 0,
or a negative 1, sorry.
And if nobody wins, tie game, we're going to say that has a score of 0,
doesn't favor one player or the other.
So now, if you are a computer trying to play moves for X,
what are you trying to do to the score?
Are you trying to maximize the score or minimize the score?
Yeah, you want to maximize the possible score.
If you have an option between a situation that will take you
to a score of negative 1 and a situation that will take you to a score of 1,
you're going to pick the one that takes you to the score of 1
because that's the one that maximizes your score.
And if you're the O player, you're going to want to do the opposite.
You're going to want to try to minimize the score,
because if you're choosing between these three options,
you want to choose the board that has the minimum score.
And so ultimately, in this game, X is going
to be trying to maximize the score and O is going
to be trying to minimize the score.
And what the Minimax algorithm is going to do
is it's a recursive algorithm that's going to figure out,
what is the move that maximizes the score
or what is the move that minimizes the score?
Questions so far, generally speaking, about where we're going
and what we're doing?
All right.
We'll take a look at this.
So we have a board that looks something like this.
This is a game that's still in play.
It's O's turn and the game is still happening.
So nobody's won just yet.
But what is the score of this board?
If you're the O player, what is the score of the board?
So think about that for a little bit.
Nobody's won just yet, but what would you say the score of this board
would be if you're O and you're trying to win?
Remember, O is trying to minimize the score.
If X wins, it's a score of 1.
If O wins, it's a score of negative 1.
If nobody wins, it's a score of 0.
OK, I hear people saying 0.
Why 0?
Someone tell me, why is this a score of 0?
What is the logic you went through to mentally get at the idea
that this is a score of 0?
Yeah?
AUDIENCE: If it's O's turn, O is going to block X [INAUDIBLE]..
BRIAN YU: OK, great.
So if it's O's turn, O is going to block X, playing in the upper right corner.
And in that case, X has to play in that top middle square.
And in that case, nobody wins and the score is 0.
Now, of course, O didn't have to play in the upper right.
O could have played in the top middle, in which case
X played in the upper right, and then X would have won the game
and that board has a score of 1.
And so what was the logic here?
We had two possible options, one option that led us to a board
with a score of 1, X winning, and one option that led us to a board
with a score of 0, nobody winning.
And between the two, we know that O should
choose the option that's going to minimize, choose
the one with the score of 0, play in the upper right,
block X from winning the game.
So let's formalize that a little bit.
What are the possible moves that X can make--
or that O could make, for instance?
Well, O could play in the top middle or O could play in the top right.
Those are the two possible options for what O can do.
And all right, let's explore what happens in each of those options.
If O plays in the top middle, then where does X play?
Well, only one option left.
X is going to play in the top right.
X is going to win that game.
And so what score will we give to this board?
AUDIENCE: 1.
BRIAN YU: 1.
Great.
That board has a score of 1.
And so what about the score of this board?
If this board needs to have a score, and the only thing that can result from it
is something with a score of 1, what score would you give this board?
Great, also 1.
Because once you get to this position, you're going to get to this position,
and this board has a score of 1.
So this board must also have a score of 1.
So now, let's explore the side.
O plays in the upper right.
When that happens, X plays in the only available square, the top middle.
And what scored does this board have?
0, great.
Nobody won that game so that board has a score of 0, which means this board has?
AUDIENCE: 0.
BRIAN YU: Great, also a score of 0.
And so now, we get to what we could call the minimization step.
We know that O has two options, one board that
leads to a score of 1, 1 board that leads to a score of 0.
What should happen?
Well, it's going to pick the one that has the score of 0, which
means this board also has a score of 0.
That's the basic idea of what the Minimax algorithm is going to do.
It's going to explore those possibilities, calculate the scores,
and when it sees that it has multiple options,
it's going to ask itself, which of those options is going to minimize the score,
for the O player, at least, and then choose that option.
Questions so far?
All right.
Oh, green smiley faces.
Wonderful.
Let's try something a little bit more complicated.
We've got this board now.
This board, it's X's turn.
And what is the score of this board if it's X's turn?
AUDIENCE: 1.
BRIAN YU: 1, great.
Why?
So why is it a score of 1.
AUDIENCE: X can win.
BRIAN YU: X can win right away.
X can play in the upper right-hand corner and X is going to win this game.
But how would a computer know that?
How would a computer be able to figure that out?
Well first, it needs to consider the possible moves.
Well, X could play in the lower left.
A computer doesn't know that it shouldn't play in the lower left.
So it needs to explore that possibility.
X plays in the lower left.
OK, what can O do next?
Well, O can play there in the top middle, in which case
X plays in the top right.
And all right, X won there.
So what score would you give this board?
1, great.
So this board also has a score of 1.
But O didn't have to play in there.
O could play in the top right, in which case X plays in the top middle,
and this board has a score of 0.
Nobody wins, which means this board also has a score of 0.
And so now, here's the interesting step.
The Minimax algorithm is recursive, in the sense
that when X is trying to make a decision about what move to make there,
it needs to think about what moves should O
make if O ends up in this board?
And if O ends up in this board, well, between the options of 0 and 1,
O is going to try to minimize the score.
So O is going to say, all right, this is the board that
gives me the minimum score, so I should play in the upper right corner.
And so that board also has a score of 0.
So in other words, if X chooses to play in the lower left,
then if both players are playing optimally,
that game is going to be tied.
And we're able to calculate that recursively
by thinking, what would O do?
What would X do?
Move after move after move.
All right, so X plays in the lower left.
That results in a score of 0.
What else could X do?
X could play in the top middle.
But what happens then?
What's the score of this board?
AUDIENCE: Negative 1.
BRIAN YU: Negative 1, great.
Because what are the possible options?
O could play in the lower left, winning immediately, a score of negative 1.
Or O could play in the upper right and that
would result in a tied game, a score of 0, which means a score of 0 up here.
But that means that if O gets into this position
and is trying to minimize the score, between 0 and negative 1,
O is always going to pick this option, the option that minimizes the score.
So that board has a score of negative 1 as well.
And then, of course, the final one, the one that all of you
probably saw immediately, obviously and intuitively,
but that a computer doesn't have any way of having some intuition about,
needs to explore the possible options first,
is just to say, all right, in the upper right, in that case,
X has won the game.
That has a score of 1.
And so between those three options, the option
of a score of 0, negative 1, and 1, we should choose
the option that maximizes the score.
We should play in the upper right.
And so that board up at the top, we can conclude, has a score of 1.
So a lot of calculation to get to what was intuition for us, the idea
that board up top has a score of 1.
And we can get at that by recursively calculating.
What's the best move in this position?
What's the best move after that?
Questions?
Yeah?
AUDIENCE: So are you using recursion on, like, two functions?
If you're going to implement it, do you have--
do you have like X's best position and then [INAUDIBLE] that position,
and then [INAUDIBLE] the X function to call O for the next step?
BRIAN YU: Yeah, good question.
So the question's about actually implementing this.
We'll take a look at some pseudocode in a moment.
But generally speaking, there are a couple ways you could do this.
You could have two different functions, one that's doing maximizing,
one that's doing minimizing.
But the code is really, very similar.
So what you probably want to do is have a function that's going to optimize
and one of the arguments, maybe a Boolean flag that says,
should I maximize this or should I minimize this?
And if I'm maximizing it, then when I consider the next player's moves,
I'm going to say, OK, try and minimize it now.
And if I'm currently trying to minimize my score,
when I'm considering my opponent's moves,
I'll say, OK, what should happen if you try to maximize that score?
So it just sort of flips back and forth, one move after another,
if you go between one player's turn and the other.
Other Questions?
Yeah?
AUDIENCE: I'm assuming there's situations where the next step is
two equally good options.
How are the [? AIGs, ?] in that case?
BRIAN YU: Yeah, good question.
What happens if there are two equally good options, two options that
both have the same score?
In that case, the [? I ?] doesn't really care.
You can pick the first one.
You can pick a random one.
You can pick whatever you want.
So certainly, that's something you could do.
Now, there are ways you can make modifications to the scoring system.
It all really boils down to what the scoring system is, whereby
right now, winning is a score of 1 for X. Losing for X
is a score of negative 1, and 0 is neutral.
You can imagine a situation where you would
give more points for winning faster, where if you can win in fewer moves,
then you should prefer that.
Right now, this algorithm doesn't care about how quickly you win.
So even if there are two in a row, if you
have some other way of guaranteeing that you'll win the game,
you might do something else instead.
And the algorithm could very reasonably choose that, and that's to be expected.
But if you want to make it so that if you can win faster,
you'll win faster, than you want to encode,
somewhere in the scoring system, some notion
of if you're winning in fewer moves, that's preferable.
Other things, questions?
How do we feel about the general idea of what this algorithm is doing?
OK, some green, some yellow.
Happy to slow down and take questions if there are any.
Otherwise, we can definitely chat about this during project time, too.
All right, let's take a look at actually some of the pseudocode,
some code we could actually write in order
to implement an algorithm like this.
So we're going to do Python-like code.
This isn't exact code, so don't try and type this into Python
and expect it to work.
But this will give you the general idea of what we're trying to do.
We're trying to Minimax a particular game based on whose turn it is.
So the first thing we need to know is, if the game is over,
if someone's already won the game, then just return the score for the game.
If no moves are possible, someone's already won the game,
then the score is finalized.
There is no option, no decision points to be made.
The game is over.
So there already is a score.
That's our base case for when the game's over that's going to,
ultimately, have some sort of score.
But then we need to say, all right, we need
to figure out what the available moves are for the game.
In the case of Tic-Tac-Toe, that's as simple as just looking for,
what are the empty squares?
But it's important to just look for the empty squares
because you don't want to consider moving into places that
are illegal moves, moves that there already is an X or an O
there, for instance.
You want to consider only the available moves for the game.
And then we'll say, OK, if it's X's turn, what should we do?
If it's X's turn, we want to maximize the score.
And so if we want to maximize the score, we
should start by assuming that the value is as small as possible.
So we're going to set the value to be negative infinity, for instance,
or negative some really big number.
And then we're going to say, OK, for every possible move,
let's consider that possible move.
The new value of this board is going to be the maximum of the current value,
which starts out as negative infinity, and whatever the score of the board
would be for if you were to make that move and it were O's turn.
So this is the real critical piece of the logic, these last two lines here,
the idea that for every possible move that X can make,
let's consider, what would the score of that move
be if you were to make that move and then let O play?
Let 0 play optimally and see what the score of that would be.
Take all of those scores and take the maximum of all of them,
because X wants to maximize the total possible score.
I'll say that again because I know it's little bit confusing.
We start by assuming the value is really, really low, because we're
trying to maximize the score.
We want to increase the score.
And then consider all the possible moves we can make and take
the maximum of the value of all those possible moves.
And how do we figure out the value of those possible moves?
Well, we recursively call the Minimax algorithm again,
considering the game board with that move having been made,
and then say, it's O's turn now and O is going
to try and do something a little bit different, which
we'll see in just a moment.
Because now else, if it's O's turn, 0 is going to do the exact opposite.
It's trying to minimize the score.
So let's start by assuming the value is really, really high,
some really, really big thing.
And then for every possible move, the value of this position
is the minimum of all of the possible moves that you could make,
and then assuming that it's X's turn to move.
And so these functions will recursively call each other.
First, it'll be X's turn.
Then it'll consider Minimax, where it's O's turn.
And that will consider situations where it's X's turn, going down and down
and down and down, considering all possible options
to be able to play optimally.
Now, this works for a game like Tic-Tac-Toe,
but is not going to work as well if you consider more complex games, where
there are more possible moves.
If you try and do this on a game like chess, for instance,
the algorithm is just going to take ages in order
to figure out the optimal move.
And so to be able to play games like that,
you need to employ some more advanced techniques.
But this is probably one of the simpler ways
to think about how to pick the best move out of a set of possible moves.
Then at the end, you return the value.
In other words, what is the score of this board?
That will tell you the ultimate score of the board, whether it's
a winning board, a losing board, or a neutral board, in which nobody can win.
This algorithm will just give you the score of the board.
Is it a win for X, win for O, or nobody wins?
If you wanted to get the actual best move
you should make, then what you care about is, which of these moves,
in this list of moves, is going to produce the maximum score?
And you can think about adding a little bit of logic
there in order to keep track of what the best possible move so far is, such
that you can then later use that move in order to figure out what move to make.
Questions?
Yeah?
AUDIENCE: Does value have to be negative or positive?
Because couldn't it just be negative 1 or negative 2 or postive 2?
BRIAN YU: Good question.
So the question is, couldn't value just being negative 2 or positive 2,
or even negative 1 and 1?
In the case of Tic-Tac-Toe, yes, where in this game,
scores are ranging from negative 1 to 1.
You can imagine, though, that if you were to generalize this
to other games, where scores could be higher
or lower, that the maximum and minimum scores could be beyond that.
For Tic-Tac-Toe, though, absolutely.
If we're only considering score within the range of negative 1 to 1,
you can use negative 1 and 1 in place of negative infinity
and positive infinity.
Yeah?
AUDIENCE: Wouldn't you get error message because you can never really
reach infinity, so that would count for infinity?
BRIAN YU: Oh, good question.
Wouldn't you get some kind of error message
if you tried to encode infinity into the program?
What exactly is happening there?
An excellent question.
While you can't actually have an infinity, Python, and many languages,
have a value that can sort of simulate infinity.
In Python, it's inside the Map library, and I
think it's called map.inf for infinity.
It's just a value that represents the idea of infinity.
And it obeys mathematical properties.
So if I say, is math.inf greater than 28?
Well, yeah, true. math.inf is greater than 28
because infinity is bigger than any conceivable number.
But if I take the minimum of 50 and math.inf, that's going to give me 50.
Because if you try and take the minimum of 50 and an infinite number,
50 is the smaller one between the two of them.
So math.inf is a value you can use to stand in for infinity, at least
in Python, and that will behave the way you would expect infinity
to behave, for instance.
Questions about any of that?
Yeah?
AUDIENCE: [INAUDIBLE].
BRIAN YU: Yeah.
So the idea here is that if I am the X player
and I'm trying to maximize the score, I care about which of my possible moves
is going to maximize my score.
So imagine for a moment that--
let's just open up a text editor.
So if I have three possible moves, one of which
leads me to a score of 0, one of which leads me to a score of 0,
and one of which leads me to a score of 1,
if I start off with a value of negative infinity,
the first thing I'm going to do is say, let
me consider the first move, the move that has a score of 0.
And value is going to be set to--
I'm trying to maximize my score--
the maximum of the current value, negative infinity, and 0.
And the maximum of negative infinity and 0 with 0, because 0
is bigger than negative infinity.
Now, I consider the second move.
Well, value is going to be the maximum of 0, the current score,
and the new score is 0.
And OK, there is no difference.
It doesn't matter whether I pick the first move or the second move,
so the value of this is 0.
And then finally, I consider the last move that has a score of 1.
So this is going to be max of 0 and 1.
And between those two, the maximum of those is 1.
So ultimately, this tells me that I should
be choosing the last move because that's the one that's
going to maximize the score.
So if I just keep taking the max over all of the possible moves I can make,
eventually, I'll end up with the maximum possible score that I can have.
Good question.
Yeah?
AUDIENCE: So if you're writing [INAUDIBLE] or something bit
more complicated, you're also going to have
to take into account multiple scenarios where
you can end up with a score-- you'd end up with a score of 1.
You'd need some way to decide between them.
BRIAN YU: Good question.
Yes, you ultimately need some way to decide between if you
have multiple things of the same score.
Likely, however you choose to implement that will have some ways by default,
going to handle that situation.
Maybe you're just taking the first one or the last one
is probably going to be the most common situation.
The algorithm doesn't really care which of those you pick.
Yeah?
AUDIENCE: What method describe this algorithm [INAUDIBLE]??
BRIAN YU: Good question.
So what makes this artificial intelligence
instead of just brute force?
This is just a brute force search.
But artificial intelligence is generally described
as just the ability of machines to be able to make decisions
that we would consider to be intelligent decisions, intelligent or smart
decisions.
And this is an example of what we might call a search algorithm, which
is one of the basic algorithms used in AI, this idea of trying
to search through a space of possible moves
to figure out what the best possible move is.
And of course, this is probably just only
scratching the surface of what artificial intelligence is
really about.
Then you later can get into more sophisticated search algorithms,
get into heuristics and other algorithms, like A star, and so forth.
There are more ways to make this even more efficient, instead of just
searching everything.
This is just giving you a little bit of a taste.
If you're interested in artificial intelligence,
this stuff seems cool to you and you'd like to do more of it,
if you're a student here at Harvard, I'd highly
recommend CS182, the artificial intelligence class,
which begins with stuff like this and dives
in and goes even further into looking at how to program even more
sophisticated artificial intelligence.
I'm happy to chat with you more about that during project time,
if you'd like.
But this is just scratching the surface, in short.
Other things?
OK, well with our remaining time, I thought I'd propose a couple of things
that you can try to do.
So let's continue working on our Tic-Tac-Toe projects.
If you're still working on making moves and being
able to detect whether someone's won the game,
feel free to keep working on that.
But then consider adding some additional features.
Add the ability to reset the game, for instance.
If you're looking for something a little bit sort of intermediate step
to be able to do, consider adding move history, keeping track
of the history of the move that you make, such that you can undo a move,
for instance.
And if you're really feeling up to the challenge-- this is going to be,
I will warn, a little bit challenging, especially at first--
if you really are up for it, try and implement a button
like let the computer make a move for me that
is going to play the optimal move in a given situation.
So you can think about how you can do that as well.
If you'd like to add to the game in other ways,
you're welcome to do that, too.
I'll show you what a finished product might look like.
Certainly, you can feel free to play with the styling of things
in order to make things look a little nicer.
I'll go ahead and run Tic-Tac-Toe, too, and go here.
All right, I've got a Tic-Tac-Toe game, whereby
the X player can play a move somewhere and the O player
can play a move somewhere.
And if the O player decides, OK wait, I didn't like that move,
you might consider adding an Undue move button that just undoes that move.
And maybe that Undue move button can work multiple times,
such that if I get to this position and X wants to undo their move,
you can click Undo again, get back to the starting position.
And you can also consider adding situations where if X goes here and O
goes here, what should X do in order to win the game?
Anyone know, in a situation like this, what the optimal move for X is?
AUDIENCE: Playing in a corner.
BRIAN YU: Playing in a corner.
Yeah, probably.
So we'll let the computer make a move.
And all right, it played in the corner.
I can try to block.
I'll play here.
I'll let the computer make the next move.
All right, it moved there.
And now, there's really no way for O to win at this point.
I can try playing here, but let the computer make a move.
And OK, X has won this game.
So a lot of possible features you have to implement.
You definitely don't have to do the Minimax AI step if you don't want to,
because it is a little bit involved, but thought
I'd introduce the algorithm so you can try it, if you would like to.
But we'll open it up.
It's flexible.
Feel free to try any of these.
If you have a project of your own in mind that you'd like to work on,
feel free to work on that, too.
And what I think we'll do is we'll take the middle section.
If you're in the front half of the middle section,
go ahead and go to room 136.
If you're in the back half of the middle section, go to room 212.
And we'll have the two section on the side stay
in the auditorium for the time being.
We'll work on project time between now.
Feel free to work until around 5:00 or so.
The staff will be here till 5:00, and we'll reconvene here tomorrow
at 10:00 AM for a look at JavaScript.