How does one develop software the "old-fashioned" way? Here is how my very first, non-TDD* version of Tic-Tac-Toe went.
First thing's first - I need a UI. How else can you play, without a UI? It's the most important thing in the game, right? My 2 options for a UI are command line or browser - since those are the 2 I know best. Well, I'm not even going to consider the command line because once I do that, I'm tied to it forever. So HTML it is.
Couple of minutes and I have some basic HTML and CSS in place; it renders a 3x3 grid, not the sexiest thing in the world, but it'll do. Now what? Well now I have to be able to "put an X" on some square. Let's attach onclick handlers to each square and just show an image when the click event comes in. Make sure to keep a counter somewhere so that the image displayed alternates between an "X" and an "O". Ignore clicks on squares that already have an image there and piece of cake and wow - 30 minutes and I'm already halfway done. That's blazing fast. And all I needed was 1 "simple" function.
So what's next? Well, the computer still needs a chance to go. Since that logic is obviously separate from the logic for human clicks, let's put the computer-move logic into a separate function, and just call that function in the last line of the click handler. Now the computer will always go after we make a move. Let's implement that logic.
First few moves are pretty easy. Also, it's pretty easy to take the winning move, and block a winning move. But for the more complicated parts, well let's play the game and see what the computer does. After playing a few games, I know the scenarios where the moves are obviously incorrect, so I tweak the "nextMove" function to do the "correct" thing for "that situation". Fire up the browser, let's play again. Ok that's better, but now I'm a step further in the game and the computer is making a stupid move there. Back to the code, handle that new situation, rinse and repeat.
Hmmm, I'm already at 4 hours - I guess I wasn't really halfway done when I was 30 minutes into it. And at this point, the nextMove function is getting quite ugly. But that's ok because it won't get much uglier, I'm almost done.
Several days later
Yes! I think I have the game working. Is it really unbeatable? Well, I can't really prove it ... but go ahead and play it, I bet you can't beat it. Just to gain a little bit of confidence, I'll have 3 of my friends play around with it for a while to see if they could beat it.
Guess what. One of them did beat it.
So now I have go back to my "nextMove" function and try to figure out where it goes wrong. This function is kind of cryptic, hard to trace even though I wrote it earlier this week. I should probably break it up, but no no, let's not touch it because I might introduce a bug. This function was never supposed to change. But anyhow, change a few things around, play a few more games myself, make sure that my change didn't cause the computer to make the wrong move in a different situation, and 6 hours later the bug has been fixed.
I wonder if my friends are still willing to sacrifice more of their time to play the game again several times. I really want to be sure I didn't break anything. So I'll just send it off to them and if they have time in the next several days (hopefully) I'll get some feedback early next week and can send it off for review.
A better solution
By now it should be obvious that this approach to software development is a good way to lose your hair, your client, and your computer out the window. There are several things here that ought to be done differently, and they come free of charge when you practice a certain method.
First thing - your friends should not be your regression and acceptance tests. Your friends should be your clients - they should already get a working, bug-free version to play for fun, not to help you do your job. You should have acceptance and unit tests that are automated so that you could run them quickly whenever you want and get immediate feedback. You should not have to wait several days for your friends to find time to play your game. You should have instant feedback.
With proper TDD, you get that.
Second thing - you should never be scared to change code. Code needs to change. Clients change their minds, you find better ways of doing things, new features make old features incompatible - code will change. You should not be scared to change your code because it may introduce bugs or break things. You need something to lean back on so that when you do change your code then that thing tells you, "Hey, you're still ok."
With proper TDD, you get that.
Third thing - "complicated" code is almost always a bad sign. If you're reading something that you've written last week and are having a hard time following what's going on, you're in muddy territory**. Refactor, and do it immediately. Don't wait or put it aside because there is "no time" - find time and clean it up. There is a very good chance that either you or someone else will have to go back to that code you've written, and if you don't take time to clean it up now then you'll spend even more time deciphering it later. Do you want to clean the dishes now, or later when the food has dried onto the plates like cement and flies patrolling your kitchen sink? You're code should be clean, legible, and well organized - always.
With proper TDD, you get that.
What am I advertising here? TDD, of course. Test Driven Development is an art that every software developer ought to learn and practice to at least some extent. Even if you don't think it's useful, even if you think it'll be a waste of time - if you have never tried it before (for real), then try it. And I'm not talking about one week or two weeks of "ok fine, I'll give this thing a shot". I mean really get yourself motivated, believe that there are many benefits to reap from this practice, and invest valuable time and energy to figure this thing out for good. Then, once you get it down, use it on at least a few projects, see the difference it makes. Treat it as a college course, or as preparation for a marathon.
And by all means don't just do it so that you can say you've done; if you approach it with that kind of attitude then you'll have wasted your time. Don't cut corners. If something is "too hard to test", don't skip it - spend hours if you must, but figure out a good way to test everything. Explore different ways of doing this, get help from others who live and breathe this, and really figure this thing out. It's worth it.
* This is not the way to do things.** There are, of course, exceptions to everything. Some embedded and real-time systems must sacrifice legibility for performance. In software that powers your car's airbags, or in certain real-time trading systems, performance is critical.