Ardu-Tac-Toe -

ardu-tac-toe-1.0.hex (35.0 KB)

Hello all,
During my (little) spare time I made a little tic-tac-toe game for arduboy.
Even if it’s a very simple game, I tried to make it a little bit more interesting with:

  • hand drawn dynamic graphic style
  • single player mode against cpu with 2 difficulty settings (easy/normal)

P.S.: normal cpu rarely makes mistake, be warned!

Screens


Downloads

Source

9 Likes

Wow, great job, I like this for multiple reasons.

The game can be compiled for windows, linux and macOS with the provided Qt Creator project.

This is super awesome and helps portability to other consoles. With my projects I try to separate the Arduboy-specific code to be able to do this too.

Furthermore I like the clarity of everything – the license, readme with screenshots, nice code, … And the use of GitLab :slight_smile:

I don’t like the tabs though :smiley:


@Agent6 could find this interesting to look at and compare to his own tic-tac-toe game. It’s always nice to look at how different people solve the same problem.

2 Likes

Thanks a lot !

I use tabs since I learnt how to code, but after a (very) long time, I’m starting to think spaces may be better for a lot of different reasons …

1 Like

Good Points

Glad to see some C++11 techniques in use.
Especially enum classes, using type aliases, member initialiser lists and auto (though there’s at least one place I don’t think auto should have been used).

It’s really good to see static_cast in use instead of C-style casting.
I think I could count on one hand the number of people here I see doing that.

Not So Good Points

I’d like to point out:

if(player == eGamePlayerMark::Cross)
	drawText(x + 28, y, "X\0");
else
	drawText(x + 28, y, "O\0");

The \0 here is redundant, string literals are null-terminated by default.
What you end up with is { 'X', '\0', '\0' } and { 'O', '\0', '\0' }.

I’m not happy about the hungarian notation on the enum type names though.
(In my opinion hungarian notation should have died decades ago.)

I don’t like the C-style approach of passing structs to functions instead of using member functions.

I don’t like the use of == true. It’s generally considered to be bad form.

And finally I think some of the expressions could do with more brackets to make the order of operations more explicit.
It’s often difficult to memorise the entire operator precedence table,
it’s easier to just use extra brackets to make the order of operations obvious.
(In fact, using brackets generally makes the order of operations clear to someone who doesn’t even know the language.)

For what it’s worth, I love tabs and detest spaces.

I think being able to chose the tab width is a feature, not a bug.
It saves warring over how many spaces to use,
and you can’t accidentally use too many or too few tabs like you can with spaces.

May the holy war continue :P.

The \0 here is redundant, string literals are null-terminated by default.

oops, forgotten typo from some earlier text print tests …

I’m not happy about the hungarian notation on the enum type names though.

It’s about readibility, I found that when working with c++ beginners, emphasizing the fact that this type (especially defined as enum class) is an enum make them more careful and understand how to use it and not confuse with other types. Though maybe the “e” is not the best choice here.

I don’t like the C-style approach of passing structs to functions instead of using member functions.

Well it’s a test. Everything was more object oriented before, and I found myself a little bit lost in the code and functions using globals or not. At the end, I found it more readable and understandable from a gameplay and logic point of view.
So in this kind of small project when every byte saving is important, I guess I will continue to test mixing C++ code style with C organization style to find a good balance.

I don’t like the use of == true .

Again, a habit of working in projects shared with a lot of developpers with different levels, when you end up modifying the code you didn’t write and people modifying the code you wrote. And in this kind of environment it is always good to be as much explicit as you can to avoid bugs or misinterpretation (and no, even correct naming is not enough).
So all of my conditional tests are fully explicit.

I think being able to chose the tab width is a feature, not a bug.

I was like that before also. But I tend to (again due to environment experience) think that being sure that the message you’re trying to pass for other developpers is more important than trying to fit to their convenience with the risk of making it harder for them to read it.

By that argument you should be putting s in front of struct types and u in front of union types to differentiate them from the fundamental types.

I think it actually worsens readability, because until you’ve seen enough types to figure out the convention you’re left wondering what the e means.
If such a convention is in-use within code it needs to be documented, otherwise it looks meaningless.

In most cases the fact a type is an enum isn’t important.
The time when it is important is when you’re understanding how to use it, and to understand how to use a type you need to see its definition anyway, at which point you’ll see whether it’s a struct or an enum class (or a union etc).

In fact, you could substitute an enum class for a struct and end up using it in exactly the same way:

// As an enum
enum class SomeEnum : int
{
	EnumValueA,
	EnumValueB,
	EnumValueC,
};

// As a class
class SomeEnum
{
private:
	int value;
	
	constexpr SomeEnum(int value) : value(value) {}
	
public:
	static constexpr SomeEnum EnumValueA = SomeEnum(0);
	static constexpr SomeEnum EnumValueB = SomeEnum(1);
	static constexpr SomeEnum EnumValueC = SomeEnum(0);
};

What really matters is the interface.

Ideally there should be almost no global objects anyway (except for named constant values).

C-style isn’t necessarily smaller.
C++ is very good at zero-cost abstractions.

In general, as long as you avoid things like inheritance and virtuality, C+±style classes+member functions are no more expensive than C-style structs+free functions.
(Rarely they can even be cheaper.)

== true isn’t explicit, it’s redundant.
x == true evaluates to x, x == false evaluates to !x.

If someone doesn’t understand that if(x == true) evaluates to if(x) then they either haven’t actually understood how conditional statements work or haven’t understood how == works.

I don’t see how using spaces passes any kind of message to developers other than “this looked alright in my editor”.

You can’t predict what editor someone uses or how wide they like to have their windows, so it’s better to use something that’s configurable rather than something rigid.

By that argument you should be putting s in front of struct types and u in front of union types to differentiate them from the fundamental types.

For struct and union, their behavior is not changing when you use them. Enum as class is still a new stuff for a lot of people. My intend is, for beginners who are not specially aware about the new behavior that enum class introduce, “be careful, it’s not the enum as you knew it before, they are different type”. It was useful several times, so I just kept it. It is maybe a bad habit.

Ideally there should be almost no global objects anyway (except for named constant values).

In the scope of an arduboy project, yes, at most you can put everything behind one single global object. Does it improve code readibility, use, update, etc. than just leaving them directly accessible?
Debatable, but my preference tends for leaving most of them accessible globally than hiding them behind functions or members.

C-style isn’t necessarily smaller.
C++ is very good at zero-cost abstractions.

Sorry that’s not really what I meant. I’m into putting more C philosophy into my C++.
Like you said most of the time there is no differences between them, so in these cases I prefer to go for the simplicity of C style, for the readability, to make it accessible for most of the people.

== true isn’t explicit, it’s redundant.

By explicit, I mean for the reader that what I’m doing, what is my intention.
In my condition, I’m explicitly saying/reminding the reader that I’m waiting x to be a boolean and nothing else. And sometimes x can be the worst thing you can’t imagine …
It may be redundant, but it costs nothing, and I found it’s easier and more straightforward to explain to beginners that in a conditionnal you’re always going to test “a value” against “another value”.

“this looked alright in my editor”.

Spaces are more predictible than tabs. Depending on the context, I’m just thinking that may be sometimes useful.

Overall all of my coding choices, even if they look a little bit awkward, are from my experience with working with beginners or little known c++ people.
I’ve seen several times that some fancy c++ code can make these people uncomfortable. As a result when they need to work on your code, they will start to write over it instead of modifying it directly because they don’t understand it.
So as an habit, I tend to simplify my c++ (and c++ more generally) to make it more accessible to the biggest audience and also to be able to explain it with very basic concepts and rules when needed.

1 Like

They’ve been around for 7 years now, so they’re not that new.

The only reason they’re declared as enum class is because the comittee hates adding new keywords unless it’s sufficiently justified.
That’s the same reason templates use class (e.g. template<class T>), it wasn’t until later that typename was permitted as well.

They’re not drastically different from regular enums.
The only differences are type safety(i.e. not being implicitly convertible to int) and scoping (i.e. requiring the scope operator to be accessed).
Otherwise they’re functionally equivalent to unscoped enums.

There’s no reason to assume that a beginner would know about structs and unions but not enum classes - they could be taught in any order.

Aside from which, you’re actually using it for unscoped enums as well, not just scoped enums.

To a degree it does.

Globals have global scope, which causes problems because you can’t necessarily reason about where they’re being used.

It might seem obvious where they’re being used if you wrote the code,
but if you didn’t write the code then it can be quite surprising to find a global suddenly turn up somewhere you weren’t expecting one.

Making the global variable a member variable of another class restricts the places where it can be accessed to member functions, places that have a reference to an instance of that class (if it’s public), and places where the variable is explicitly passed to a function.

All of these are better for reusaibility because it changes the relationship from a fixed relationship to a single variable, to a more flexible relationship that can apply to different variables.

C style is that is not inherantly simpler or more readable.
function(value0, value1, value2) is not inherantly simpler or more readable than value1.function(value0, value2).

That’s more a problem with the practice of using if(x) on non-bools.
Other languages sensibly don’t allow conditional statements to accept anything but boolean types and dont’ allow implicit boolean conversion.

The solution is to avoid using if on non-bools, not to add redundancy.

That’s not true though, a conditional expression doesn’t always compare one value with another.
In telling them that, you’re potentially setting them up for a fall later when they encounter code that breaks that assumption.

I wouldn’t call object.method() ‘fancy’ - it’s a common syntax found in a multitude of languages.
By comparison, the type inference of auto is much more ‘fancy’.

That’s not a problem with the code, that’s a problem with the person’s approach.
If someone doesn’t understand something then they should seek to understand it before they try to change it.
If people run away from what they don’t understand then they don’t learn anything.

They can simply ask how it works, and why - communication is an invaluable tool.

Even restricting code to a simpler subset doesn’t guarantee that everyone will understand it.

Arguably you could simplify the code further by getting rid of type inference with auto and using C-style casting instead of static_cast and reinterpret_cast, but every time you opt for a simplification it tends to come with a cost.

auto wouldn’t be too much of an issue since it’s mainly a tool for laziness (with a few meaningful applications when writing templates and avoiding redundancy),
but getting rid of static_cast and reinterpret_cast would discard something very valuable - an expression of intent, and would open up a chance for potentially dangerous bugs to slip in (e.g. casting away const by accident).

Would making the code marginally more appealing to people who don’t understand those features be worth the trouble it would cause the people who do understand them?
Would it be worth denying the people who don’t understand a learning opportunity?

We may continue to argue about what is important and what is not, what is logical and what is not, but what is important, I guess, is that I’m not doing anything “wrong” here.

I’m convinced that C++ is an intimidating programming language for a lot of people with a lot of rules, rule exceptions, etc… So reducing to a smaller simplier subset of easy “to understand and to apply” rules that works in 99% of the time, as a start, really helps most people to focus and achieve faster what they want.

I’m trying to put that in my coding style. It may not be perfect but it’s in a constant evolution everyday and for every project I’m working on (adding/removing/changing stuff) and up to now and from experience, that’s what works the most in my working environment.

2 Likes

@zeduckmaster I’ve had this discussion with @Pharap too and basically I’m on your side here.

Let’s be fair, a lot of this discussion is about psychology, philosophy, hypothetical situations etc. These talks can go on forever. And have a high probability of not ending very well :smiley:

About e.g. the if (x == true) I’d probably agree with @Pharap at first, because a lot of times I find shorter expressions to be more readable (again, psychology, individual perception), but reading your arguments about being explicit also makes a lot of sense to me and I simply haven’t thought about it this way. I think it’s never black and white. There are different values in conflict and finding the balance is extremely difficult.

To me the important thing is you’ve made a really nice working program with clear source code. I think we can also all agree that one of the most important things is being consistent in whatever style you choose, which is the case here. So I’m very happy about your contribution. I’ve also got to play it now and the “sketchy” art style is really awesome.

3 Likes