I started working on this project of a chess game out of the interest of computer graphics and game engines, and as a pratice of programming a larger project in general. Because my focus is on learning, I explicitly decided to code almost everything from scratch without libraries. It took me roughly about two months' spare time to get to this stage, which includes a simple game engine, various file format reader, a simple font rasterizer, a chess AI (not working very well so disabled for now), and more. Though I decided to stop the project for an indefinite time, it's an experience I think quite important to me. I will summarize my learnings below.
For any project beyond daily work, the number one thing is to maintain the morale. A particular important point I think is to achieve something everyday, preferably a new feature. Being able to see something constructed everyday is a source of joy that will keep you going. Although this sounds trivial, it has further implications that will impact one's coding practice. To achieve something concrete in the most efficient way means to try one's best to avoid unnecessary work. For example, under this philosophy one should prefer simple and specific solutions over complicated and general solutions, and only scale up the solution when it's necessary. Another example is one should not over-refactor code as long as the current tangling code doesn't obstruct work.
There's also the more intrinsic aspect of morale. I think inevitably interest will degrade at some stage, so one's initial excitement about a project will gradually turn empty. I think this is the main reason why personal projects in the end don't get to the level of quality planned. Therefore if a project is taken seriously, the why question should be answered ahead of time. Because for any project of reasonable achievements, a large amount of time needs to be invested. A rational reason instead of an emotional reason will be more helpful in maintaining morale over a long period of time.
Debugging an algorithm manually can be quite challenging, but sometimes we can let computer help. A particular bug that I remembered is after I finished implementing the convex hull algorithm, the algorithm just didn't work and generated some weird results. Following the program states in the debugger wasn't effective, since it meant you need to keep track of hundreds of floating point numbers and try to make sense of them. Coming up some simple test cases wasn't very effective either, since first it was not clear what are the simple cases for 3D models, and second the bug might just not reveal itself on the simple test cases. Finally I found the bug by writing some high level assertions of the properties that should hold at certain points, and the program then found the bug for me. I feel this is a technique that's underused by me and should be explored much more.
A proper way to view algorithm is to treat it a bit like mathematics, in the sense that the result of code should always be well-defined. In some programming topics, one seems to be able to get the program work without the level of rigor necessary. Two examples I can think of are concurrency and floating point operations. For concurrency, an algorithm is only correct when race conditions are guaranteed not to happen. For floating point operations, an algorithm is only correct when certain level of precision can be guaranteed. The font rasterizer I wrote still suffers from numerical instability, which results in incorrect anti-aliasing sometimes. In both cases there's a tendency to ignore the details and leave the algorithm in a 'roughly working state'. But fundamentally speaking, the algorithm isn't even correct. Therefore if an algorithm is taken seriously, we should really put enough work to guarantee the correctness.
One shouldn't be afraid of code refactoring. I think there's some psychological resistence to deleting the existing code and write some new code to fit into the current codebase. The process means temporarily getting outside of the program's working state, so the resistence probably comes from the fear of not being able to get back to the working state. However, this kind of fear is not necessary. At the worst case, you can reimplement the entire program if you don't know how to transform to the new solution in small steps. So when a refactoring need is identified, one should just jump right in without thinking too much. The actual path of refactoring will unfold itself when one wades through the messiness of code. We should have confidence in that.
Code can be downloaded here if you're interested.
The executable can be downloaded here. No guarantee it works on your environment, but you can try it on Windows 10 with Vulkan driver.