初始化提交

This commit is contained in:
王立帮
2024-07-20 22:09:06 +08:00
commit c247dd07a6
6876 changed files with 2743096 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
#OS junk files
[Tt]humbs.db
*.DS_Store
#.gitignore
#dOxygen project documentation and related files
#[Dd]oc/html/
#[Mm]edia/photo/
[Mm]edia/music/
[Mm]edia/video/
#Stuff
*.zip
#Visual Studio files
*.sln
*.vcxproj
*.filters
*.vsarduino.h
*.[Oo]bj
*.user
*.aps
*.pch
*.vspscc
*.vssscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh
*.bak
*.[Cc]ache
*.ilk
*.log
*.lib
*.sbr
*.sdf
*.opensdf
*.unsuccessfulbuild
*.vmps.xml
ipch/
[Oo]bj/
[Bb]in
[Dd]ebug*/
[Rr]elease*/
Ankh.NoLoad
#Video files
*.mov
*.mp3
*.mp4
*.wlmp
#Pelles C files
*.ppj
*.tag
*.ppx
#Additional Atmel Studio files
*.atsln
*.atsuo
*.cppproj
#Project files
[Bb]uild/
# Office Temp Files
~$*
# visual studio database projects
*.dbmdl

View File

@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -0,0 +1,9 @@
# MD_MAXPanel LED Matrix Panel Display Library
[Video of the library features](https://www.youtube.com/watch?v=DUwtmrIVH58)
The library implements functions that allow a panel of MAX72xx modules to be used as a pixel display device. The library provides support for supports elements such as points, lines, rectangles, triangles circles and text. Polygons can also be displayed as filled polygons.
If you like and use this library please consider making a small donation using [PayPal](https://paypal.me/MajicDesigns/4USD)
[Library Documentation](https://majicdesigns.github.io/MD_MAXPanel/)

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,652 @@
// Implements the game of Bricks using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - bat left switch, INPUT_PULLUP
// RIGHT_PIN - bat right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - unused
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Player must keep the ball in play by batting it away. Each 'brick' that
// is hit scores points. Once all the bricks are cleared the game continues
// with a new screen. Game end when the ball is let out of bounds.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized INPUT_PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialised in setup()
const uint16_t FIELD_LEFT = 1;
const uint8_t BRICK_SIZE_DEFAULT = 3;
const uint8_t BAT_SIZE_DEFAULT = 3; // must be an odd number
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "BRICKS";
const uint16_t SPLASH_DELAY =3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint8_t MAX_LIVES = 9;
const uint16_t MAX_SCORE = 999;
// A class to encapsulate the bricks bat
// Bat runs at the bottom of the display to keep the ball in play
class cBrickBat
{
private:
uint16_t _x, _y; // the position of the center of the bat
uint16_t _xmin, _xmax; // the max and min bat boundaries
int8_t _vel; // the velocity of the bat (+1 for moving up, -1 moving down)
uint8_t _size; // the size in pixels for the bat (odd number)
uint8_t _pinLeft; // the pin for the up switch
uint8_t _pinRight; // the pin for th down switch
uint16_t _batDelay; // the delay between possible moves of the bat in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the bat
public:
enum hitType_t { NO_HIT, CORNER_HIT, FLAT_HIT };
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t xmax, uint8_t size, uint8_t pinL, uint8_t pinR)
{
_x = x;
_y = y;
_xmin = xmin;
_xmax = xmax;
_vel = 0;
_size = size;
_pinLeft = pinL;
_pinRight = pinR;
_batDelay = 40;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
PRINTXY("\nbat @", _x, _y);
PRINTXY(" limits", _xmin, _xmax);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
int8_t getVelocity(void) { return(_vel); }
void draw(void) { mp.drawHLine(_y, _x - (_size / 2), _x + (_size / 2), true); }
void erase(void) { mp.drawHLine(_y, _x - (_size / 2), _x + (_size / 2), false); }
bool anyKey(void) { return(digitalRead(_pinLeft) == LOW || digitalRead(_pinRight) == LOW); }
hitType_t hit(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
int16_t dx = x1 - x0;
// if we are not in the same y plane as the bat there can be no hit
if (y1 != _y) return(NO_HIT);
// if the ball is at
// - the left of the bat and travelling left to right, or
// - the right of the bat and travelling right to left
// then it is a corner hit
if ((x1 == _x + (_size / 2) && dx < 0) || (x1 == _x - (_size / 2) && dx>0))
return(CORNER_HIT);
// the ball is between the left and right bat boundaries inclusive
// then it is a flat hit
if (x1 <= _x + (_size / 2) && x1 >= _x - (_size / 2))
return(FLAT_HIT);
// and in case we missed something, a default no hit
return(NO_HIT);
}
bool move(void)
{
bool moveLeft = (digitalRead(_pinLeft) == LOW);
bool moveRight = (digitalRead(_pinRight) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _batDelay)
return(false);
_timeLastMove = millis();
mp.update(false);
if (moveRight)
{
PRINTS("\n-- BAT move Left");
erase();
_vel = 1;
_x++;
if (_x + (_size/2) > _xmax) _x--; // keep within top boundary
draw();
}
else if (moveLeft)
{
PRINTS("\n-- BAT move right");
erase();
_vel = -1;
_x--;
if (_x - (_size/2) < _xmin) _x++; // keep within bottom boundary
draw();
}
else
_vel = 0;
mp.update(true);
return((moveLeft || moveRight));
}
};
// A class to encapsulate the bricks ball
// Ball bounces off the bat, edges and bricks
class cBrickBall
{
private:
uint16_t _x, _y; // the position of the center of the ball
int8_t _dx, _dy; // the offsets for the x and y direction
uint16_t _xmin, _ymin; // minimum bounds for the ball
uint16_t _xmax, _ymax; // maximum bounds for the ball
uint32_t _timeLastMove; // last time the ball was moved
uint16_t _ballDelay; // the delay between ball moves in milliseconds
bool _run; // ball is running when true
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_TOP, BOUNCE_BOTTOM, BOUNCE_LEFT, BOUNCE_RIGHT };
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax)
{
_dx = _dy = 1;
_timeLastMove = 0;
_ballDelay = 100;
_xmin = xmin;
_xmax = xmax;
_ymin = ymin;
_ymax = ymax;
_run = false;
reset(x, y);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
uint16_t getNextX(void) { return (_x + _dx); }
uint16_t getNextY(void) { return (_y + _dy); }
uint16_t getDelay(void) { return (_ballDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _ballDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void draw(void) { mp.setPoint(_x, _y, true); } // PRINTXY("\nball@", _x, _y); }
void erase(void) { mp.setPoint(_x, _y, false); }
void reset(uint16_t x, uint16_t y) { _x = x; _y = y; _dy = 1; }
bool move(void)
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _ballDelay)
return(false);
_timeLastMove = millis();
// do the animation
mp.update(false);
erase();
_x = _x + _dx;
_y = _y + _dy;
// ensure it always stays in bounds
if (_x < _xmin) { _x = _xmin; _dx = -_dx; }
if (_x > _xmax) { _x = _xmax; _dx = -_dx; }
if (_y < _ymin) { _y = _ymin; _dy = -_dy; }
if (_y > _ymax) { _y = _ymax; _dy = -_dy; }
// now update
draw();
mp.update(true);
return(true);
}
void bounce(bounce_t b)
{
switch (b)
{
case BOUNCE_TOP:
case BOUNCE_BOTTOM: _dy = -_dy; break;
case BOUNCE_LEFT:
case BOUNCE_RIGHT: _dx = -_dx; break;
case BOUNCE_BACK: _dx = -_dx; _dy = -_dy; break;
}
}
};
// A class to encapsulate the entire field of bricks
// All the bricks are contained in this object.
class cBrickField
{
private:
struct brick_t
{
uint16_t x, y; // leftmost coordinate for this brick
brick_t *next; // the next in the linked list
};
uint8_t _size; // size of the bricks
brick_t *_bricks; // start of the list
brick_t *_deleted; // the deleted list
void draw(brick_t *pb) { mp.drawHLine(pb->y, pb->x, pb->x + _size-1, true); }
void erase(brick_t *pb) { mp.drawHLine(pb->y, pb->x, pb->x + _size-1, false); }
void add(uint16_t x, uint16_t y)
// add a brick to the start of the list
{
brick_t *pb;
if (_deleted == nullptr)
pb = new brick_t;
else
{
// take from the front of the deleted list
pb = _deleted;
_deleted = _deleted->next;
}
pb->x = x;
pb->y = y;
pb->next = _bricks;
_bricks = pb;
return(pb);
}
void del(brick_t *pbPrev, brick_t *pb)
// delete the specified brick from the list
{
if (pbPrev == nullptr)
_bricks = pb->next;
else
pbPrev->next = pb->next;
// save to the front of the deleted list
pb->next = _deleted;
_deleted = pb;
}
void dumpList(void)
{
PRINTS("\nDUMP List ===");
for (brick_t *pb = _bricks; pb != nullptr; pb = pb->next)
{
PRINTXY("\n", pb->x, pb->y);
draw(pb);
}
PRINTS("\n===");
}
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_UP, BOUNCE_DOWN };
void begin(uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax, uint8_t size)
{
// work out how many bricks we can put in the space
const uint8_t gapX = 2;
const uint8_t gapY = 3;
uint8_t marginSide = 2;
const uint8_t marginTop = gapY;
const uint8_t numAcross = (xmax - xmin - marginSide - marginSide + size) / (gapX + size);
const uint8_t numDown = (ymax - ymin - marginTop) / gapY;
// now adjust the side margin to center the display as much as possible
marginSide = (1 + (xmax - xmin) - ((numAcross - 1)*gapX) - (numAcross*size))/2;
PRINT("\nBricks size=", size);
PRINTXY(" field = ", xmin, ymin);
PRINTXY(" ", xmax, ymax);
PRINT(" -> Across=", numAcross);
PRINT(" Down=", numDown);
PRINT(" Adj margin=", marginSide);
_size = size;
_bricks = _deleted = nullptr;
// create all the bricks
uint16_t x = xmin + marginSide;
for (uint8_t i = 0; i < numAcross; i++)
{
uint16_t y = ymax - marginTop;
PRINT("\nin column ", i);
for (uint8_t j = 0; j < numDown; j++)
{
add(x, y);
PRINTXY(" - ", x, y);
y -= gapY;
}
x += (gapX + size);
}
//dumpList(); // debug to verify the list is created properly
}
bool emptyField(void) { return(_bricks == nullptr); }
void drawField(void) { for (brick_t *pb = _bricks; pb != nullptr; pb = pb->next) draw(pb); }
void eraseField(void) { while (_bricks != nullptr) { erase(_bricks); del(nullptr, _bricks); } }
bounce_t checkHits(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
brick_t *pb = _bricks;
brick_t *pbPrev = nullptr;
int16_t dx = x2 - x1;
int16_t dy = y2 - y1;
bounce_t b = BOUNCE_NONE;
while (pb != nullptr)
{
if (y2 == pb->y) // at the same height as this brick
{
// check one of the corners with sideways approach
if ((x2 == pb->x && dx > 0) || (x2 == pb->x - _size - 1 && dx < 0))
{
PRINTS("\n-! BRICK hit on corner");
b = BOUNCE_BACK;
}
// check the whole flat surface with any approach
else if (x2 >= pb->x && x2 <= pb->x + _size - 1)
{
if (dy < 0)
{
b = BOUNCE_UP;
PRINTS("\n-! BRICK hit on top");
}
else
{
b = BOUNCE_DOWN;
PRINTS("\n-! BRICK hit on bottom");
}
}
// eliminate this brick
if (b != BOUNCE_NONE)
{
PRINTXY("\n-! Ball @", x1, y1);
PRINTXY(" next@", x2, y2);
PRINTXY(" brick ", pb->x, pb->y);
PRINTXY("-", pb->x+_size-1, pb->y);
PRINTS(" - deleting");
erase(pb);
del(pbPrev, pb);
break; // no point looping further - only one brick per hit
}
}
// advance the pointers
pbPrev = pb;
pb = pb->next;
}
return(b);
}
};
// main objects coordinated by the code logic
cScore lives, score;
cBrickBall ball;
cBrickBat bat;
cBrickField bricks;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
bat.draw();
lives.draw();
score.draw();
ball.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Bricks]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax() - 1;
bat.begin((FIELD_RIGHT - FIELD_LEFT) / 2, BAT_EDGE_OFFSET, FIELD_LEFT + 1, FIELD_RIGHT - 1, BAT_SIZE_DEFAULT, LEFT_PIN, RIGHT_PIN);
ball.begin(bat.getX(), bat.getY() + 1, FIELD_LEFT+1, 0, FIELD_RIGHT-1, FIELD_TOP-1);
lives.begin(&mp, FIELD_LEFT + 1, FIELD_TOP + 1 + mp.getFontHeight(), MAX_LIVES);
score.limit(MAX_SCORE); // set width() so we can used it below
score.begin(&mp, FIELD_RIGHT - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
sound.begin(BEEPER_PIN);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_POINT_PLAY, S_BALL_OUT, S_BRICKS_EMPTY, S_POINT_RESET, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
ball.reset(bat.getX(), bat.getY() + 1);
lives.set(MAX_LIVES);
score.reset();
setupField();
bricks.begin(FIELD_LEFT + 1, FIELD_TOP / 3, FIELD_RIGHT - 1, FIELD_TOP - 1, BRICK_SIZE_DEFAULT);
bricks.drawField();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (bat.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
ball.start();
runState = S_POINT_PLAY;
PRINTSTATE("POINT_PLAY");
}
break;
case S_POINT_PLAY: // playing a point
// handle the bat animation first
bat.move();
// now move the ball and check what this means
if (ball.move())
{
cBrickBat::hitType_t lastHit;
// check for top/bottom edge collisions
if (ball.getX() <= FIELD_LEFT + 1)
{
PRINTS("\n-- COLLISION left edge");
ball.bounce(cBrickBall::BOUNCE_LEFT);
sound.bounce();
}
else if (ball.getX() >= FIELD_RIGHT - 1)
{
PRINTS("\n-- COLLISION right edge");
ball.bounce(cBrickBall::BOUNCE_RIGHT);
sound.bounce();
}
if (ball.getY() >= FIELD_TOP - 1)
{
PRINTS("\n-- COLLISION top edge");
ball.bounce(cBrickBall::BOUNCE_TOP);
sound.bounce();
}
// check for bat collisions
if ((lastHit = bat.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cBrickBat::NO_HIT)
{
PRINTS("\n-- COLLISION bat");
ball.bounce(lastHit == cBrickBat::CORNER_HIT ? cBrickBall::BOUNCE_BACK : cBrickBall::BOUNCE_BOTTOM);
sound.hit();
}
// check for out of bounds at the bottom
if (ball.getY() < BAT_EDGE_OFFSET)
{
PRINTS("\n-- OUT!");
runState = S_BALL_OUT;
}
// check for any hits to bricks
cBrickField::bounce_t b;
if ((b = bricks.checkHits(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cBrickField::BOUNCE_NONE)
{
score.increment();
sound.bounce();
// check if this is the end of all the bricks
if (bricks.emptyField())
{
PRINTS("\n== BRICKS empty");
runState = S_BRICKS_EMPTY;
}
else
{
// otherwise we need to bounce the ball in the right direction
switch (b)
{
case cBrickField::BOUNCE_UP: ball.bounce(cBrickBall::BOUNCE_BOTTOM); break;
case cBrickField::BOUNCE_DOWN: ball.bounce(cBrickBall::BOUNCE_TOP); break;
case cBrickField::BOUNCE_BACK: ball.bounce(cBrickBall::BOUNCE_BACK); break;
}
}
}
}
break;
case S_BRICKS_EMPTY: // handle the bricks being completed
PRINTSTATE("BRICKS_EMPTY");
ball.stop();
ball.erase();
lives.increment();
ball.setDelay(ball.getDelay() - 5);
sound.point();
bricks.begin(FIELD_LEFT + 1, FIELD_TOP / 3, FIELD_RIGHT - 1, FIELD_TOP - 1, BRICK_SIZE_DEFAULT);
bricks.drawField();
runState = S_POINT_RESET;
break;
case S_BALL_OUT: // handle the ball going out
PRINTSTATE("BALL_OUT");
ball.stop();
ball.erase();
lives.decrement();
sound.point();
if (lives.score() != 0)
runState = S_POINT_RESET;
else
runState = S_GAME_OVER;
break;
case S_POINT_RESET:
PRINTSTATE("POINT_RESET");
bat.draw();
ball.reset(bat.getX(), bat.getY() + 1);
ball.draw();
delay(500);
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
bricks.eraseField();
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,98 @@
// Program to exercise the MD_MAXPanel library
//
// Displays a rotating 3D cube, shamelessly adapted from the
// Microview library example "MicroViewCube.ino"
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
#include <MD_MAXPanel.h>
#include <SPI.h>
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
// We may wait a bit between updates of the display
const uint16_t ROTATION_DELAY = 0; // in milliseconds
float d = 3;
float px[] = { -d, d, d, -d, -d, d, d, -d };
float py[] = { -d, -d, d, d, -d, -d, d, d };
float pz[] = { -d, -d, -d, -d, d, d, d, d };
float p2x[] = {0,0,0,0,0,0,0,0};
float p2y[] = {0,0,0,0,0,0,0,0};
float r[] = {0,0,0};
void setup()
{
mp.begin();
mp.clear();
}
void loop()
{
drawCube();
delay(ROTATION_DELAY);
}
void drawCube()
{
const uint16_t SHAPE_SIZE = 40 * (min(mp.getXMax(), mp.getYMax()) / 3);
const uint16_t SCREEN_WIDTH = mp.getXMax();
const uint16_t SCREEN_HEIGHT = mp.getYMax();
r[0]=r[0]+PI/180.0; // Add a degree
r[1]=r[1]+PI/180.0; // Add a degree
r[2]=r[2]+PI/180.0; // Add a degree
if (r[0] >= 360.0*PI/180.0) r[0] = 0;
if (r[1] >= 360.0*PI/180.0) r[1] = 0;
if (r[2] >= 360.0*PI/180.0) r[2] = 0;
for (uint8_t i = 0; i < 8; i++)
{
float px2 = px[i];
float py2 = cos(r[0])*py[i] - sin(r[0])*pz[i];
float pz2 = sin(r[0])*py[i] + cos(r[0])*pz[i];
float px3 = cos(r[1])*px2 + sin(r[1])*pz2;
float py3 = py2;
float pz3 = -sin(r[1])*px2 + cos(r[1])*pz2;
float ax = cos(r[2])*px3 - sin(r[2])*py3;
float ay = sin(r[2])*px3 + cos(r[2])*py3;
float az = pz3-150;
p2x[i] = SCREEN_WIDTH/2+ax*SHAPE_SIZE/az;
p2y[i] = SCREEN_HEIGHT/2+ay*SHAPE_SIZE/az;
}
mp.clear();
mp.update(false);
for (uint8_t i = 0; i < 3; i++)
{
mp.drawLine(p2x[i],p2y[i],p2x[i+1],p2y[i+1]);
mp.drawLine(p2x[i+4],p2y[i+4],p2x[i+5],p2y[i+5]);
mp.drawLine(p2x[i],p2y[i],p2x[i+4],p2y[i+4]);
}
mp.drawLine(p2x[3],p2y[3],p2x[0],p2y[0]);
mp.drawLine(p2x[7],p2y[7],p2x[4],p2y[4]);
mp.drawLine(p2x[3],p2y[3],p2x[7],p2y[7]);
mp.update(true);
}

View File

@@ -0,0 +1,217 @@
// Implements Conway's Game of Life using MD_MAXPanel library
//
// Hardware used
// =============
// Momentary On push switch on SWITCH_PIN to start a new game. The digital I/O
// will be initialize INPUT_PULLUP
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// The universe of the Game of Life is an infinite, two-dimensional orthogonal
// grid of square cells, each of which is in one of two possible states,
// alive or dead (or populated and unpopulated). Every cell interacts with
// its eight neighbors, which are the cells that are horizontally, vertically,
// or diagonally adjacent. At each step in time, the following transitions occur:
//
// 1. Any live cell with fewer than two live neighbors dies, as if by under population.
// 2. Any live cell with two or three live neighbors lives on to the next generation.
// 3. Any live cell with more than three live neighbors dies, as if by overpopulation.
// 4. Any dead cell with exactly three live neighbors becomes a live cell, as if
// by reproduction.
//
// The initial pattern constitutes the seed of the system. The first generation is
// created by applying the above rules simultaneously to every cell in the seed;
// births and deaths occur simultaneously, and the discrete moment at which this
// happens is sometimes called a tick. Each generation is a pure function of the
// preceding one. The rules continue to be applied repeatedly to create further
// generations.
//
#include <MD_MAXPanel.h>
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(F(")")); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#endif
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
#define SWITCH_PIN 6
// We always wait a bit between updates of the display
#define TICK_TIME 150 // in milliseconds
void setup(void)
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel Game of Life]");
pinMode(SWITCH_PIN, INPUT_PULLUP);
mp.begin();
mp.clear();
randomSeed(seedOut(31, RANDOM_SEED_PORT));
}
void loop(void)
{
static uint32_t timeLastRun = 0;
static uint8_t sameCount = 10;
static uint32_t lastCount = 0;
uint32_t count = countCells();
if (lastCount == count) sameCount++; else sameCount = 0;
if (digitalRead(SWITCH_PIN) == LOW || sameCount >= 10)
{
mp.clear(); // mark the end of the display ...
delay(1000); // ... with a minor pause!
firstGeneration();
sameCount = 0;
}
lastCount = count;
// Check if next generation time
if (millis() - timeLastRun >= TICK_TIME)
{
timeLastRun = millis();
nextGeneration();
}
}
uint32_t countCells(void)
{
uint32_t count = 0;
for (uint16_t x = 0; x <= mp.getXMax(); x++)
for (uint16_t y = 0; y <= mp.getYMax(); y++)
count += (mp.getPoint(x,y) ? 1:0);
return(count);
}
void firstGeneration(void)
// Create a 4-way symmetric random setup
{
mp.update(false);
PRINTS("\n-- FIRST Generation");
PRINTXY("\n-- Field size (1,1) - ", mp.getXMax() - 1, mp.getYMax() - 1);
mp.clear();
for (uint16_t x=1; x<(mp.getXMax()+1) / 2; x++)
for (uint16_t y = 1; y < (mp.getYMax()+1) / 2; y++)
{
bool b = (random(101) > 50);
mp.setPoint(x, y, b);
mp.setPoint(mp.getXMax() - x, y, b);
mp.setPoint(x, mp.getYMax() - y, b);
mp.setPoint(mp.getXMax() - x, mp.getYMax() - y, b);
}
mp.update(true);
}
void nextGeneration(void)
// Apply the rules
{
bool rowBuf[2][mp.getXMax()+2];
uint16_t count;
bool newCell;
PRINTS("\n-- NEW generation");
// clear out the row buffers
memset(rowBuf, 0, sizeof(rowBuf));
mp.update(false);
for (uint16_t y=mp.getYMax()-1; y>=1; y--)
{
// copy the current row to the buffer
for (uint16_t x=0; x<mp.getXMax(); x++)
rowBuf[1][x] = mp.getPoint(x, y);
// work out a 'new' current row
for (uint16_t x=1; x<mp.getXMax(); x++)
{
// count the number of neighbours
count = rowBuf[0][x-1] ? 1:0;
count += rowBuf[0][x] ? 1:0;
count += rowBuf[0][x+1]? 1:0;
count += rowBuf[1][x-1]? 1:0;
count += rowBuf[1][x+1]? 1:0;
count += mp.getPoint(x-1, y-1)? 1:0;
count += mp.getPoint(x, y-1) ? 1:0;
count += mp.getPoint(x+1, y-1)? 1:0;
PRINTXY("\n@", x, y);
PRINT(" count=", count);
PRINTS(" ->");
if (count < 2)
{
// A live cell with fewer than two neighbors dies.
newCell = false;
PRINTS("dies.");
}
else if ((count == 2 || count == 3) && mp.getPoint(x, y))
{
// A live cell with two or three neighbors lives on.
newCell = true;
PRINTS("stays.");
}
else if (count == 3 && !mp.getPoint(x, y))
{
// A dead cell with exactly three neighbors becomes live.
newCell = true;
PRINTS("born.");
}
else
{
// A live cell with more than three neighbors dies.
newCell = false;
PRINTS("dies.");
}
mp.setPoint(x, y, newCell);
}
// update the saved row buffers
for (uint16_t x=0; x<=mp.getXMax(); x++)
rowBuf[0][x] = rowBuf[1][x];
}
mp.update(true);
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,824 @@
// Implements the game of Meteors using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - bat left switch, INPUT_PULLUP
// RIGHT_PIN - bat right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - unused
// SELECT_PIN - shooting bullets switch, INPUT_PULLUP
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// The player controls a spaceship in an meteor field. The object of the
// game is to shoot and destroy meteors while not colliding with any meteor
// fragments. Meteors start as a large size and become smaller and move
// faster as they fragment when they are hit. Smaller, faster meteors
// score more points that large slow ones. The game ends when a meteor
// hits the spaceship, destroying it.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialized in setup()
const uint16_t FIELD_LEFT = 0;
const uint8_t BRICK_SIZE_DEFAULT = 3;
const uint8_t BAT_SIZE_DEFAULT = 3; // must be an odd number
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "METEOR";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = 999;
const uint16_t START_SCORE = 25;
const uint8_t ROID_BIG_SIZE = 4;
const uint8_t ROID_MID_SIZE = 2;
const uint8_t ROID_SML_SIZE = 1;
const uint8_t MAX_BULLETS = 5;
// A class to encapsulate the shooter
// Shooter is at the bottom of the display shooting bullets upwards
class cGun
{
private:
uint16_t _x, _y; // the position of the tip of the gun
uint16_t _xmin, _xmax; // the max and min gun boundaries
uint8_t _pinLeft; // the pin for the left switch
uint8_t _pinRight; // the pin for the right switch
uint16_t _moveDelay; // the delay between possible moves of the gun in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the gun
public:
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t xmax, uint8_t pinL, uint8_t pinR)
{
_x = x;
_y = y;
_xmin = xmin;
_xmax = xmax;
_pinLeft = pinL;
_pinRight = pinR;
_moveDelay = 25;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
void draw(void) { mp.setPoint(_x, _y, true); mp.drawHLine(_y-1, _x-1, _x+1, true); }
void erase(void) { mp.setPoint(_x, _y, false); mp.drawHLine(_y-1, _x-1, _x+1, false); }
bool anyKey(void) { return(digitalRead(_pinLeft) == LOW || digitalRead(_pinRight) == LOW); }
bool move(void)
{
bool moveLeft = (digitalRead(_pinLeft) == LOW);
bool moveRight = (digitalRead(_pinRight) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _moveDelay)
return(false);
_timeLastMove = millis();
mp.update(false);
if (moveRight)
{
//PRINTS("\n-- GUN move right");
erase();
_x++;
if (_x + 1 > _xmax) _x--; // keep within right boundary
draw();
}
else if (moveLeft)
{
//PRINTS("\n-- GUN move left");
erase();
_x--;
if (_x - 1 < _xmin) _x++; // keep within left boundary
draw();
}
mp.update(true);
return((moveLeft || moveRight));
}
bool checkHits(uint16_t x, uint16_t y)
{
bool b = false;
if (_y >= y)
b = (_x == x) || ((y == _y - 1) && (x == _x - 1 || x == _x + 1));
return(b);
}
};
// A class to encapsulate all the bullets
// Bullets are fired from the gun and move straight upwards
class cBullet
{
private:
typedef struct bullet_t
{
uint16_t x, y; // the position of the center of the bullet
uint32_t timeLastMove; // time bullet last moved
bullet_t *next; // next in the list
};
uint8_t _pinShoot; // the shooting switch pin
uint16_t _ymin, _ymax; // maximum bounds for the bullet
uint32_t _moveDelay; // delay between bullets moves in milliseconds
uint16_t _shootDelay; // the delay between shots in milliseconds
uint32_t _timeLastShoot; // the last time the gun was shot in milliseconds
bullet_t *_bullets; // the queue of bullets being fired
bullet_t *_deleted; // delete bullets list
bullet_t *_pScan; // for getFirst(), getNext()
uint8_t _count; // count the number of bullets
bullet_t *add(uint16_t x, uint16_t y)
{
bullet_t* pb = nullptr;
if (_count < MAX_BULLETS)
{
if (_deleted == nullptr)
{
pb = new bullet_t;
PRINTS(" new");
}
else
{
// take one from the front of the deleted queue
pb = _deleted;
_deleted = _deleted->next;
PRINTS(" recyle");
}
// save the data
if (pb != nullptr)
{
_count++;
pb->x = x;
pb->y = y;
pb->timeLastMove = 0;
pb->next = _bullets;
_bullets = pb;
}
}
return(pb);
}
void del(bullet_t *pbPrev, bullet_t * pb)
{
if (pbPrev == nullptr)
_bullets = pb->next;
else
pbPrev->next = pb->next;
_count--;
// save to the front of the deleted list
pb->next = _deleted;
_deleted = pb;
}
public:
void begin(uint16_t ymin, uint16_t ymax, uint8_t pinShoot)
{
_moveDelay = 50;
_shootDelay = _moveDelay * 3;
_ymin = ymin;
_ymax = ymax;
_deleted = _bullets = nullptr;
_pinShoot = pinShoot;
_count = 0;
pinMode(_pinShoot, INPUT_PULLUP);
}
bool getFirstXY(uint16_t &x, uint16_t &y)
{
_pScan = _bullets;
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
bool getNextXY(uint16_t &x, uint16_t &y)
{
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
void draw(bullet_t *pb) { if (pb != nullptr) mp.setPoint(pb->x, pb->y, true); } //PRINTXY("\nbullet @", pb->x, pb->y); }
void erase(bullet_t *pb) { if (pb != nullptr) mp.setPoint(pb->x, pb->y, false); }
void reset(void) { while (_bullets != nullptr) { erase(_bullets); del(nullptr, _bullets); }}
bool empty(void) { return (_bullets == nullptr); }
void kill(uint16_t x, uint16_t y)
// search for and kill the bullet at (x, y)
{
bullet_t *pb = _bullets;
bullet_t *pbPrev = nullptr;
while (pb!= nullptr)
{
if (pb->x == x && pb->y == y)
{
erase(pb);
del(pbPrev, pb);
break; // found it, no need to look further
}
// advance pointers
pbPrev = pb;
pb = pb->next;
}
}
void move(void)
{
bullet_t *pb = _bullets;
bullet_t *pbPrev = nullptr;
mp.update(false);
while (pb != nullptr)
{
// is this the time for a move?
if (millis() - pb->timeLastMove >= _moveDelay)
{
pb->timeLastMove = millis();
// do the animation
erase(pb);
pb->y++;
if (pb->y >= FIELD_TOP)
{
PRINTS("\n-- BULLET ending");
del(pbPrev, pb);
pb = pbPrev; // we have just deleted pb!
}
else
draw(pb);
}
// advance pointers
pbPrev = pb;
if (pb != nullptr) pb = pb->next;
}
mp.update(true);
}
bool shoot(uint16_t x, uint16_t y)
// shoot the next bullet if switch is pressed
{
bool b = false;
// if this is the time for a move?
if (millis() - _timeLastShoot <= _shootDelay)
return(b);
_timeLastShoot = millis();
// now check if the switch is pressed
if (digitalRead(_pinShoot) == LOW)
{
PRINTS("\n-- BULLET shoot");
b = (add(x, y) != nullptr);
}
return(b);
}
};
// A class to encapsulate all the meteors
class cMeteors
{
private:
typedef struct meteor_t
{
// NOTE - these are uint8_t to save RAM
uint8_t x, y; // lower tip coordinate for this meteor
uint8_t dx, dy; // speed in the x, y direction
uint8_t size; // size of the meteor (BIG, MID, SML)
uint32_t timeLastMove;// when this meteor last moved
meteor_t *next; // the next in the linked list
};
uint32_t _timeTick; // base time multiplied by size
uint32_t _timeLastBorn; // last time an meteor was born
uint32_t _timeGestation; // time between meteors being born
meteor_t *_meteors; // start of the list
meteor_t *_deleted; // the deleted list
meteor_t *_pScan; // for getFirst(), getNext()
void drawBig(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
if ((pa->y + 1) < FIELD_TOP)
{
mp.drawHLine(pa->y + 1, pa->x - 1, pa->x + 1, b);
if ((pa->y + 2) < FIELD_TOP)
mp.setPoint(pa->x, pa->y + 2, b);
}
}
void drawMid(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
if ((pa->y + 1) < FIELD_TOP)
{
mp.setPoint(pa->x, pa->y + 1, b);
}
}
void drawSml(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
}
void draw(meteor_t *pa)
{
//PRINTXY("\nmeteor @", pa->x, pa->y);
if (pa == nullptr) return;
switch (pa->size)
{
case ROID_BIG_SIZE: drawBig(pa, true); break;
case ROID_MID_SIZE: drawMid(pa, true); break;
case ROID_SML_SIZE: drawSml(pa, true); break;
}
}
void erase(meteor_t *pa)
{
if (pa == nullptr) return;
switch (pa->size)
{
case ROID_BIG_SIZE: drawBig(pa, false); break;
case ROID_MID_SIZE: drawMid(pa, false); break;
case ROID_SML_SIZE: drawSml(pa, false); break;
}
}
meteor_t *add(uint16_t x, uint16_t y)
// add a brick to the start of the list
{
meteor_t *pa = nullptr;
if (_deleted == nullptr)
{
pa = new meteor_t;
PRINTS("new");
}
else
{
// take from the front of the deleted list
pa = _deleted;
_deleted = _deleted->next;
PRINTS("recyle");
}
if (pa != nullptr)
{
pa->x = x;
pa->y = y;
pa->dx = 0;
pa->dy = -1;
pa->size = ROID_BIG_SIZE;
pa->timeLastMove = 0;
pa->next = _meteors;
_meteors = pa;
}
return(pa);
}
void del(meteor_t *paPrev, meteor_t *pa)
// delete the specified brick from the list
{
if (paPrev == nullptr)
_meteors = pa->next;
else
paPrev->next = pa->next;
// save to the front of the deleted list
pa->next = _deleted;
_deleted = pa;
}
bool generate(void)
{
if (millis() - _timeLastBorn < _timeGestation)
return(false);;
_timeLastBorn = millis();
if (_timeGestation > 500) _timeGestation -= 10; // speed up the number born over the game duration
{
uint16_t x = FIELD_LEFT + 2 + random(FIELD_RIGHT - FIELD_LEFT - 4);
uint16_t y = FIELD_TOP - 1;
PRINTXY("\n-- ASTEROID create @", x, y);
add(x, y);
}
return(true);
}
public:
void begin()
{
_meteors = _deleted = nullptr;
_timeTick = 250;
_timeGestation = 5000;
}
bool emptyField(void) { return(_meteors == nullptr); }
void drawField(void) { for (meteor_t *pa = _meteors; pa != nullptr; pa = pa->next) draw(pa); }
void eraseField(void) { while (_meteors != nullptr) { erase(_meteors); del(nullptr, _meteors); } }
bool getFirstXY(uint16_t &x, uint16_t &y)
{
_pScan = _meteors;
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
bool getNextXY(uint16_t &x, uint16_t &y)
{
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
void move(void)
{
meteor_t *pa = _meteors;
meteor_t *paPrev = nullptr;
mp.update(false);
generate(); // create a new one if time to do so
while (pa != nullptr)
{
// is this the time for a move?
if (millis() - pa->timeLastMove >= _timeTick * pa->size)
{
pa->timeLastMove = millis();
// do the animation
erase(pa);
// y==0 neede to persist after the increment so the collision code can find it.
// Time to deletw it now before calculating moves.
if (pa->y == 0)
{
PRINTS("\n-- ASTEROID ending bottom");
del(paPrev, pa);
pa = paPrev; // we have just deleted pa!
}
else
{
pa->y += pa->dy;
pa->x += pa->dx;
if (pa->x <= FIELD_LEFT || pa->x >= FIELD_RIGHT)
{
PRINTS("\n-- ASTEROID ending side");
del(paPrev, pa);
pa = paPrev; // we have just deleted pa!
}
else
draw(pa);
}
}
// advance pointers
paPrev = pa;
if (pa != nullptr) pa = pa->next;
}
mp.update(true);
}
uint8_t checkHits(uint16_t x, uint16_t y)
// Returns the points for the meteor hit
{
meteor_t *pa = _meteors;
meteor_t *paPrev = nullptr;
meteor_t *paNew;
uint8_t points = 0;
bool notFound = true;
//PRINTXY("\n--- CHECKHITS for ", x, y);
while (pa != nullptr && notFound)
{
switch (pa->size)
{
case ROID_SML_SIZE: // front on point hit only
if (pa->x == x && pa->y == y)
{
//PRINTXY("\n-- ASTEROID SML hit @", pa->x, pa->y);
// smallest one just gets deleted
erase(pa);
del(paPrev, pa);
points = 4;
notFound = false;
}
break;
case ROID_MID_SIZE: // front on point hit only
if (pa->x == x && pa->y == y)
{
//PRINTXY("\n-- ASTEROID MID hit @", pa->x, pa->y);
// needs to be split
erase(pa);
paNew = add(pa->x, pa->y);
pa->dx = -1;
paNew->dx = 1;
pa->size = paNew->size = ROID_SML_SIZE;
draw(pa);
draw(paNew);
points = 2;
notFound = false;
}
break;
case ROID_BIG_SIZE: // front on point and either side hits
if ((pa->x == x && pa->y == y) ||
((pa->x - 1 == x || pa->x + 1 == x) && (pa->y + 1 == y)))
{
//PRINTXY("\n-- ASTEROID BIG hit @", pa->x, pa->y);
// needs to be split
erase(pa);
paNew = add(pa->x, pa->y);
pa->dx = -1;
paNew->dx = 1;
pa->size = paNew->size = ROID_MID_SIZE;
draw(pa);
draw(paNew);
points = 1;
notFound = false;
}
break;
default:
PRINT("\n--- CHECKHIT UNKNOWN size=", pa->size);
break;
}
// advance the pointers
paPrev = pa;
if (pa != nullptr) pa = pa->next;
}
return(points);
}
};
// main objects coordinated by the code logic
cScore score;
cGun gun;
cBullet bullets;
cMeteors meteors;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
gun.draw();
score.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Bricks]");
randomSeed(seedOut(31, RANDOM_SEED_PORT));
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
//mp.setRotation(MD_MAXPanel::ROT_90);
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax();
gun.begin((FIELD_RIGHT - FIELD_LEFT) / 2, BAT_EDGE_OFFSET, FIELD_LEFT + 1, FIELD_RIGHT - 1, LEFT_PIN, RIGHT_PIN);
bullets.begin(1, FIELD_TOP-1, SELECT_PIN);
score.limit(MAX_SCORE); // set width() so we can use it below
score.begin(&mp, FIELD_RIGHT - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
sound.begin(BEEPER_PIN);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_GAME_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax() - border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - border, mp.getYMax() - border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight()) / 2, TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
bullets.reset();
score.set(START_SCORE);
setupField();
meteors.begin();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (gun.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
runState = S_GAME_PLAY;
PRINTSTATE("GAME_PLAY");
}
break;
case S_GAME_PLAY: // playing a point
// handle shooting first
if (score.score() != 0) // we have some bullets ...
if (bullets.shoot(gun.getX(), gun.getY() + 1)) // ... and we can shoot ...
{
score.decrement(); // ... then we have one bullet less
sound.hit();
}
// then run animations
gun.move();
meteors.move();
bullets.move();
// now check for bullet collisions
{
uint16_t x, y;
//PRINTS("\n--- CHECK bullets collisions");
if (bullets.getFirstXY(x, y))
{
do
{
uint8_t points = meteors.checkHits(x, y);
if (points != 0)
{
//PRINT(", hit=", points);
bullets.kill(x, y); // bullet is now dead
//PRINTS(", kill");
sound.bounce();
//PRINTS(", sound");
score.increment(points);
//PRINTS(", score");
}
} while (bullets.getNextXY(x, y));
}
// now check for gun collisions
//PRINTS("\n--- CHECK guns collisions");
if (meteors.getFirstXY(x, y))
{
do
{
if (gun.checkHits(x, y))
runState = S_GAME_OVER;
} while (meteors.getNextXY(x, y) && runState != S_GAME_OVER);
}
}
//PRINTS("\n--- CHECK collisions done");
// finally, have we reached the end of the line?
if (bullets.empty() && score.score() == 0)
runState = S_GAME_OVER;
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
meteors.eraseField();
bullets.reset();
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,466 @@
// Implements the game of Pong using MD_MAXPanel library
//
// Hardware Used
// =============
// UP_PIN - left bat up switch, INPUT_PULLUP
// RIGHT_PIN - right bat up switch, INPUT_PULLUP
// RIGHT_PIN - left bat up switch, INPUT_PULLUP
// DOWN_PIN - right bat up switch, INPUT_PULLUP
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Simple game like table tennis where each player has to bat the ball to keep
// it in play. Points awarded when the ball goes out at the opponents side. First
// to reach MAX_SCORE is the winner.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#endif
// Hardware pin definitions.
// All momentary on switches are initialised INPUT_PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP; // needs to be initialised in setup()
const uint16_t FIELD_BOTTOM = 0;
const uint8_t BAT_SIZE_DEFAULT = 3;
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "PONG";
const uint16_t SPLASH_DELAY = 5000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint8_t MAX_SCORE = 11;
// A class to encapsulate the pong bat
// Bats are used either side of the display, moving up and down
class cPongBat
{
private:
uint16_t _x, _y; // the position of the center of the bat
uint16_t _ymin, _ymax; // the max and min bat boundaries
int8_t _vel; // the velocity of the bat (+1 for moving up, -1 moving down)
uint8_t _size; // the size in pixels for the bat (odd number)
uint8_t _pinUp; // the pin for the up switch
uint8_t _pinDown; // the pin for th down switch
uint16_t _batDelay; // the delay between possible moves of the bat in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the bat
public:
enum hitType_t { NO_HIT, CORNER_HIT, FLAT_HIT };
void begin(uint16_t x, uint16_t y, uint16_t ymin, uint16_t ymax, uint8_t size, uint8_t pinU, uint8_t pinD)
{
_x = x;
_y = y;
_ymin = ymin;
_ymax = ymax;
_vel = 0;
_size = size;
_pinUp = pinU;
_pinDown = pinD;
_batDelay = 40;
pinMode(_pinUp, INPUT_PULLUP);
pinMode(_pinDown, INPUT_PULLUP);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
int8_t getVelocity(void) { return(_vel); }
void draw(void) { mp.drawVLine(_x, _y - (_size / 2), _y + (_size / 2), true); }
void erase(void) { mp.drawVLine(_x, _y - (_size / 2), _y + (_size / 2), false); }
bool anyKey(void) { return((digitalRead(_pinUp) == LOW) || (digitalRead(_pinDown) == LOW)); }
hitType_t hit(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
int16_t dy = y1 - y0;
// if we are not in the same x plane as the bat there can be no hit
if (x1 != _x) return(NO_HIT);
// if the ball is at
// - the top of the bat and traveling down, or
// - the bottom of the bat and traveling up
// then it is a corner hit
if ((y1 == _y + (_size / 2) && dy < 0) || (y1 == _y - (_size / 2) && dy>0))
return(CORNER_HIT);
// the ball is between the top and bottom of the bat boundaries inclusive
// then it is a flat hit
if (y1 <= _y + (_size / 2) && y1 >= _y - (_size / 2))
return(FLAT_HIT);
// and in case we missed something, a default no hit
return(NO_HIT);
}
void move(void)
{
bool moveUp = (digitalRead(_pinUp) == LOW);
bool moveDown = (digitalRead(_pinDown) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _batDelay)
return;
_timeLastMove = millis();
mp.update(false);
if (moveUp)
{
PRINTS("\n-- BAT move up");
erase();
_vel = 1;
_y++;
if (_y + (_size/2) > _ymax) _y--; // keep within top boundary
draw();
}
else if (moveDown)
{
PRINTS("\n-- BAT move down");
erase();
_vel = -1;
_y--;
if (_y - (_size/2) < _ymin) _y++; // keep within bottom boundary
draw();
}
else
_vel = 0;
mp.update(true);
}
};
// A class to encapsulate the pong ball
// Ball will bounce around. Bounces off the edges and bats.
class cPongBall
{
private:
uint16_t _x, _y; // the position of the center of the ball
int8_t _dx, _dy; // the offsets for the x and y direction
uint32_t _timeLastMove; // last time the ball was moved
uint16_t _ballDelay; // the delay between ball moves in milliseconds
bool _run; // ball is running when true
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_TOP, BOUNCE_BOTTOM, BOUNCE_LEFT, BOUNCE_RIGHT };
void begin(uint16_t x, uint16_t y)
{
reset(x, y);
_dx = _dy = 1;
_timeLastMove = 0;
_ballDelay = 100;
_run = false;
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
uint16_t getNextX(void) { return (_x + _dx); }
uint16_t getNextY(void) { return (_y + _dy); }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void draw(void) { mp.setPoint(_x, _y, true); } //PRINTS("\nball@"); PRINTXY(_x, _y); }
void erase(void) { mp.setPoint(_x, _y, false); }
void reset(uint16_t x, uint16_t y) { _x = x; _y = y; }
bool move(void)
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _ballDelay)
return(false);
_timeLastMove = millis();
// do the animation
mp.update(false);
erase();
_x = _x + _dx;
_y = _y + _dy;
draw();
mp.update(true);
return(true);
}
void bounce(bounce_t b)
{
switch (b)
{
case BOUNCE_TOP:
case BOUNCE_BOTTOM: _dy = -_dy; break;
case BOUNCE_LEFT:
case BOUNCE_RIGHT: _dx = -_dx; break;
case BOUNCE_BACK: _dx = -_dx; _dy = -_dy; break;
}
}
};
// main objects coordinated by the code logic
cPongBat batL, batR;
cScore scoreL, scoreR;
cPongBall ball;
cSound sound;
void centerLine(void)
// Dotted line down the middle
{
bool bOn = true;
mp.update(false);
for (uint16_t y = FIELD_BOTTOM + 1; y < FIELD_TOP; y++)
{
mp.setPoint(mp.getXMax() / 2, y, bOn);
mp.setPoint((mp.getXMax() + 1) / 2, y, bOn);
bOn = !bOn;
}
mp.update(true);
}
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_BOTTOM, 0, mp.getXMax());
mp.drawHLine(FIELD_TOP, 0, mp.getXMax());
centerLine();
batL.draw();
batR.draw();
scoreL.draw();
scoreR.draw();
ball.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Pong]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
sound.begin(BEEPER_PIN);
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_GAME_START, S_POINT_PLAY, S_POINT_END, S_WAIT_LSTART, S_WAIT_RSTART, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialise for a new game
batL.begin(BAT_EDGE_OFFSET, (FIELD_TOP - FIELD_BOTTOM) / 2, FIELD_BOTTOM + 1, FIELD_TOP - 1, BAT_SIZE_DEFAULT, UP_PIN, LEFT_PIN);
batR.begin(mp.getXMax() - BAT_EDGE_OFFSET, (FIELD_TOP - FIELD_BOTTOM) / 2, FIELD_BOTTOM + 1, FIELD_TOP - 1, BAT_SIZE_DEFAULT, RIGHT_PIN, DOWN_PIN);
scoreL.begin(&mp, BAT_EDGE_OFFSET, FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
scoreR.limit(MAX_SCORE); // set width() used below
scoreR.begin(&mp, mp.getXMax() - (scoreR.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
ball.begin((mp.getXMax() / 2) - BAT_EDGE_OFFSET - 1, (FIELD_TOP - FIELD_BOTTOM) / 3);
setupField();
runState = S_GAME_START;
break;
case S_GAME_START: // waiting for the start of a new game
if (batL.anyKey() || batR.anyKey())
{
PRINTS("\n-- Starting Game");
scoreL.reset();
scoreR.reset();
sound.start();
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_POINT_PLAY: // playing a point
// handle the bat animation first
batL.move();
batR.move();
// now move the ball and check what this means
if (ball.move())
{
cPongBat::hitType_t lastHit;
// redraw the centerline if the ball is near it
if (ball.getX() >= (mp.getXMax() / 2) - 1 || ball.getX() >= (mp.getXMax() / 2) + 2)
{
centerLine();
ball.draw();
}
// check for top/bottom edge collisions
if (ball.getY() == FIELD_TOP - 1)
{
PRINTS("\n-- COLLISION top edge");
ball.bounce(cPongBall::BOUNCE_TOP);
sound.bounce();
}
else if (ball.getY() == FIELD_BOTTOM + 1)
{
PRINTS("\n-- COLLISION bottom edge");
ball.bounce(cPongBall::BOUNCE_BOTTOM);
sound.bounce();
}
// check for bat collisions
if ((lastHit = batL.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cPongBat::NO_HIT)
{
PRINTS("\n-- COLLISION left bat");
ball.bounce(lastHit == cPongBat::CORNER_HIT ? cPongBall::BOUNCE_BACK: cPongBall::BOUNCE_LEFT);
sound.hit();
}
else if ((lastHit = batR.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cPongBat::NO_HIT)
{
PRINTS("\n-- COLLISION right bat");
ball.bounce(lastHit == cPongBat::CORNER_HIT ? cPongBall::BOUNCE_BACK : cPongBall::BOUNCE_RIGHT);
sound.hit();
}
// check for out of bounds
if ((ball.getX() < BAT_EDGE_OFFSET) || (ball.getX() > mp.getXMax() - BAT_EDGE_OFFSET))
{
PRINTS("\n-- OUT!");
runState = S_POINT_END;
}
}
break;
case S_POINT_END: // handle the ball going out
ball.stop();
batL.draw();
batR.draw();
sound.point();
delay(500);
ball.erase();
if (ball.getX() < BAT_EDGE_OFFSET) // out on the left side
{
PRINTS("\n--- LEFT side");
ball.reset(BAT_EDGE_OFFSET + 1, batL.getY());
ball.bounce(cPongBall::BOUNCE_LEFT);
scoreR.increment();
if (scoreR.score() == MAX_SCORE)
runState = S_GAME_OVER;
else
runState = S_WAIT_LSTART;
}
else // out on the right side
{
PRINTS("\n--- RIGHT side");
ball.reset(mp.getXMax() - BAT_EDGE_OFFSET - 1, batR.getY());
ball.bounce(cPongBall::BOUNCE_RIGHT);
scoreL.increment();
if (scoreL.score() == MAX_SCORE)
runState = S_GAME_OVER;
else
runState = S_WAIT_RSTART;
}
ball.draw();
break;
case S_WAIT_LSTART: // waiting for left playter to restart the game
if (batL.anyKey())
{
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_WAIT_RSTART: // waiting fo the right player to restrt the game
if (batR.anyKey())
{
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_GAME_OVER:
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT))/2, (FIELD_TOP - FIELD_BOTTOM)/2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, (FIELD_TOP - FIELD_BOTTOM)/2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,507 @@
// Implements the game of Snake using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - left move switch, INPUT_PULLUP
// UP_PIN - up move switch, INPUT_PULLUP
// DOWN_PIN - down move switch, INPUT_PULLUP
// RIGHT_PIN - right move switch, INPUT_PULLUP
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Guide the snake around the screen using the direction keys. Running into
// a pill will increase the length of the snake. Running into the sides or
// the snake will end the game.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 1
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialised in setup()
const uint16_t FIELD_LEFT = 0;
const uint16_t FIELD_BOTTOM = 0;
const uint8_t SNAKE_SIZE_DEFAULT = 2;
const char TITLE_TEXT[] = "SNAKE";
const uint16_t SPLASH_DELAY =3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t MAX_LENGTH = 999;
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = MAX_LENGTH;
const uint16_t MAX_FOOD = 99;
// A class to encapsulate the snake direction switches
// Can move up, down, left, right
class cMoveSW
{
private:
uint8_t _pinLeft;
uint8_t _pinRight;
uint8_t _pinUp;
uint8_t _pinDown;
public:
enum moveType_t { MOVE_NONE, MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT };
void begin(uint8_t pinL, uint8_t pinR, uint8_t pinU, uint8_t pinD)
{
_pinLeft = pinL;
_pinRight = pinR;
_pinUp = pinU;
_pinDown = pinD;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
pinMode(_pinUp, INPUT_PULLUP);
pinMode(_pinDown, INPUT_PULLUP);
}
bool anyKey(void) { return (move() != MOVE_NONE);}
moveType_t move(void)
{
if (digitalRead(_pinLeft) == LOW)
return(MOVE_LEFT);
else if (digitalRead(_pinRight) == LOW)
return(MOVE_RIGHT);
else if (digitalRead(_pinUp) == LOW)
return(MOVE_UP);
else if (digitalRead(_pinDown) == LOW)
return(MOVE_DOWN);
return(MOVE_NONE);
}
};
// A class to encapsulate the food pill
class cPill
{
private:
uint16_t _x, _y; // pill coordinates
uint16_t _xmin, _xmax; // boundaries for the creation of the pill
uint16_t _ymin, _ymax;
uint8_t _value; // value of the pill
public:
void begin(uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax)
{
_value = 0;
_xmin = xmin;
_xmax = xmax;
_ymin = ymin;
_ymax = ymax;
PRINTXY("\n-- PILL boundaries ", _xmin, _ymin);
PRINTXY(" to ", _xmax, _ymax);
}
uint16_t getX(void) { return(_x); }
uint16_t getY(void) { return(_y); }
uint8_t getValue(void) { return(_value); }
void reset(void)
{
do // search for an unused space
{
_x = _xmin + (random(_xmax - _xmin + 1));
_y = _ymin + (random(_ymax - _ymin + 1));
//PRINTXY("\n--- PILL TEST ", _x, _y);
} while (mp.getPoint(_x, _y));
_value = random(10);
PRINTXY("\n-- PILL @", _x, _y);
PRINT(" worth ", _value);
mp.setPoint(_x, _y, true);
}
};
// A class to encapsulate the snake
class cSnake
{
private:
struct snakeNode_t
{
uint8_t x, y; // coordinate for this body segment. This is UINT*_T to save
// RAM, could overflow with larger displays
snakeNode_t *next; // the next in the linked list
};
snakeNode_t *_head; // the head segment of the snake
snakeNode_t *_tail; // the tail segment of the snake
snakeNode_t *_deleted; // deleted blocks list
int8_t _dx, _dy; // the movement offsets for the x and y direction
uint32_t _timeLastMove; // last time the snake was moved
uint16_t _moveDelay; // the delay between moves in milliseconds
bool _run; // snake is moving when true
cScore *_pFoodCount; // the food counter
void draw(uint16_t x, uint16_t y) { mp.setPoint(x, y, true); }
void erase(uint16_t x, uint16_t y) { mp.setPoint(x, y, false); }
snakeNode_t *add(uint16_t x, uint16_t y)
// add a body segment to the tail
// the list is stored from tail to head
{
snakeNode_t *psn;
if (_deleted == nullptr)
psn = new snakeNode_t;
else
{
// take one from the head of the deleted list
psn = _deleted;
_deleted = _deleted->next;
}
if (psn != nullptr)
{
psn->x = x;
psn->y = y;
if (_tail == nullptr)
{
psn->next = nullptr;
_head = _tail = psn; // the first segment
PRINTS("\n-- SNAKE added first element");
}
else
{
psn->next = _tail;
_tail = psn;
PRINTS("\n-- SNAKE added element");
}
}
return(psn);
}
void deleteAll()
// delete all the body segments in the snake
{
snakeNode_t *psn;
uint16_t count = 0;
while (_tail != nullptr)
{
// adjust the linked list by consuming from the tail
psn = _tail;
_tail = _tail->next;
// put the block in the deleted list
psn->next = _deleted;
_deleted = psn;
count++;
}
_head = nullptr;
PRINT("\n-- SNAKE nodes deleted: ", count);
}
public:
enum moveType_t { MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT };
uint16_t getHeadX(void) { return (_head->x); }
uint16_t getHeadY(void) { return (_head->y); }
uint16_t getDelay(void) { return (_moveDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _moveDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void setDirection(moveType_t d)
{
switch (d)
{
case MOVE_UP: _dx = 0; _dy = 1; break;
case MOVE_DOWN: _dx = 0; _dy = -1; break;
case MOVE_LEFT: _dx = -1; _dy = 0; break;
case MOVE_RIGHT: _dx = 1; _dy = 0; break;
}
}
void reset(uint16_t x, uint16_t y)
{
deleteAll();
for (uint8_t i = 0; i < SNAKE_SIZE_DEFAULT; i++)
{
snakeNode_t *psn = add(x - i, y);
if (psn != nullptr) draw(psn->x, psn->y);
}
setDirection(MOVE_RIGHT);
_pFoodCount->reset();
}
void begin(cScore *pFoodCount)
{
_dx = 1;
_dy = 0;
_timeLastMove = 0;
_moveDelay = 100;
_run = false;
_pFoodCount = pFoodCount;
_pFoodCount->reset();
_deleted = nullptr;
}
bool move(void)
// return true if something was hit
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _moveDelay)
return(false);
_timeLastMove = millis();
// do the animation
snakeNode_t *psn = _tail;
bool hitSomething = false;
// check if we would be colliding with the snake body
while (psn != nullptr && !hitSomething)
{
hitSomething = (psn->x == _head->x + _dx && psn->y == _head->y + _dy);
psn = psn->next;
}
if (hitSomething) PRINTS("\n-! COLLIDE snake");
if (!hitSomething)
{
psn = _tail; // reset back to the start of the data
// do we need to add one?
if (_pFoodCount->score() != 0)
{
add(_tail->x, _tail->y); // make a copy of the tail
_pFoodCount->decrement();
}
else // delete the tail on the display
erase(_tail->x, _tail->y);
while (psn != nullptr)
{
if (psn->next != nullptr) // main body segment
{
psn->x = psn->next->x;
psn->y = psn->next->y;
}
else // the head
{
psn->x += _dx;
psn->y += _dy;
// check if this is already used
hitSomething = (mp.getPoint(psn->x, psn->y));
draw(psn->x, psn->y);
}
psn = psn->next;
}
if (hitSomething) PRINTS("\n-! COLLIDE not snake");
}
return(hitSomething);
}
};
// main objects coordinated by the code logic
cScore score, food;
cPill pill;
cSnake snake;
cMoveSW moveSW;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawHLine(FIELD_BOTTOM, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
score.draw();
food.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Snake]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
randomSeed(seedOut(31, RANDOM_SEED_PORT));
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax();
pill.begin(FIELD_LEFT + 1, FIELD_BOTTOM + 1, FIELD_RIGHT - 1, FIELD_TOP - 1);
food.begin(&mp, FIELD_LEFT + 1, FIELD_TOP + 1 + mp.getFontHeight(), MAX_FOOD);
sound.begin(BEEPER_PIN);
score.limit(MAX_SCORE); // set the width so we can use it below
score.begin(&mp, FIELD_RIGHT - ((score.width() * (FONT_NUM_WIDTH) + mp.getCharSpacing())) - mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
moveSW.begin(LEFT_PIN, RIGHT_PIN, UP_PIN, DOWN_PIN);
snake.begin(&food);
}
bool doSwitches(void)
{
bool b = true;
switch (moveSW.move())
{
case cMoveSW::MOVE_UP: snake.setDirection(cSnake::MOVE_UP); break;
case cMoveSW::MOVE_DOWN: snake.setDirection(cSnake::MOVE_DOWN); break;
case cMoveSW::MOVE_LEFT: snake.setDirection(cSnake::MOVE_LEFT); break;
case cMoveSW::MOVE_RIGHT: snake.setDirection(cSnake::MOVE_RIGHT); break;
case cMoveSW::MOVE_NONE: b = false; break;
}
return(b);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_POINT_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
score.reset();
food.reset();
setupField();
snake.reset((FIELD_RIGHT - FIELD_LEFT) / 2, (FIELD_TOP - FIELD_BOTTOM) / 2);
pill.reset();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (doSwitches())
{
PRINTS("\n-- Starting Game");
sound.start();
snake.start();
runState = S_POINT_PLAY;
PRINTSTATE("POINT_PLAY");
}
break;
case S_POINT_PLAY: // playing a point
// handle the switches
doSwitches();
// move snake and check what this means
if (snake.move())
{
if (snake.getHeadX() == pill.getX() && snake.getHeadY() == pill.getY()) // have we hit the pill?
{
sound.hit();
score.increment(pill.getValue());
food.increment(pill.getValue());
pill.reset();
}
else // we have hit the wall or ourselves
{
snake.stop();
runState = S_GAME_OVER;
}
}
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,427 @@
// Implements the game of TTT using MD_MAXPanel library
// Adapted from the LCD example in the MD_TTT library
//
// Hardware used
// =============
// LEFT_PIN - unused
// UP_PIN - unused
// DOWN_PIN - unused
// RIGHT_PIN - unused
// SELECT_PIN - switch moves from one selection to another,INPUT_PULLUP
// ENTER_PIN - accepts the current selection, INPUT_PULLUP
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
// MD_TTT available from https://github.com/MajicDesigns/MD_TTT
//
// Rules of the Game
// =================
// Tic-tac-toe (also known as noughts and crosses or Xs and Os) is a
// paper-and-pencil game for two players, X and O, who take turns marking
// the spaces in a 3×3 grid. The player who succeeds in placing three
// of their marks in a horizontal, vertical, or diagonal row wins the
// game. This version has the computer as one of the players.
//
#include <MD_TTT.h>
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "MD_MAXPanel_TTT_Types.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#define PRINTSTATE(s)
#endif
// function prototype
void tttCallback(uint8_t position, int8_t player);
// User switches for gameplay
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// Miscellaneous defines
const uint16_t FLASH_DELAY = 500; // milliseconds
const uint8_t FLASH_REPEAT = 5; // number of flashes
const char TITLE_TEXT[] = "TTT";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
// Coordinates for a board position
boardCoord_t movePos[TTT_BOARD_SIZE] = { 0 };
// Handling for switch states (User Input)
swState_t swAccept = { ENTER_PIN, false, 0 };
swState_t swSelect = { SELECT_PIN, false, 0 };
// Main objects used defined here
MD_TTT TTT(tttCallback);
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES); // SPI hardware interface
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES); // Arbitrary pins
int8_t curPlayer = TTT_P2;
bool inGamePlay = false;
// variables and constants used for the display
uint16_t USER_MESG, GRID_BOTTOM, GRID_RIGHT, GRID_TOP;
uint8_t squareSize, offsetX, offsetY;
void calcGridSize(void)
{
uint16_t dx = (GRID_RIGHT - 2) / 3;
uint16_t dy = (GRID_TOP - GRID_BOTTOM - 2) / 3;
squareSize = min(dx, dy);
offsetX = ((GRID_RIGHT - 2) - (squareSize * 3)) / 2; // offset from vert edge
offsetY = ((GRID_TOP - GRID_BOTTOM - 2) - (squareSize * 3)) / 2; // offset from horiz edge
GRID_BOTTOM = GRID_TOP - offsetY - (3 * squareSize) - 2;
PRINT("\nsquareSize = ", squareSize);
PRINT(", offsetX = ", offsetX);
PRINT(", offsetY = ", offsetY);
// set up the board square coordinates
for (uint8_t j = 0; j < TTT_BOARD_SIZE / 3; j++)
for (uint8_t i = 0; i < TTT_BOARD_SIZE / 3; i++)
{
uint8_t idx = (j * 3) + i;
movePos[idx].x = offsetX + (i*squareSize) + i; // last element is for lines
movePos[idx].y = GRID_TOP - offsetY - (j*squareSize) - j; // last element is for lines
movePos[idx].size = squareSize-1;
PRINT("\nmovePos[", idx);
PRINTXY("] - ", movePos[idx].x, movePos[idx].y);
PRINT(" size=", movePos[idx].size);
}
}
void displayGrid(void)
{
mp.update(false);
mp.clear();
mp.drawVLine(offsetX + squareSize, GRID_BOTTOM + offsetY, GRID_TOP - offsetY);
mp.drawVLine(offsetX + (2*squareSize) + 1, GRID_BOTTOM + offsetY, GRID_TOP - offsetY);
mp.drawHLine(GRID_TOP - offsetY - squareSize, offsetX, GRID_RIGHT - offsetX);
mp.drawHLine(GRID_TOP - offsetY - (2*squareSize) - 1, offsetX, GRID_RIGHT - offsetX);
mp.update(true);
}
void highlightCell(uint8_t cell, bool b = true)
{
mp.drawRectangle(movePos[cell].x, movePos[cell].y,
movePos[cell].x + movePos[cell].size, movePos[cell].y - movePos[cell].size, b);
}
bool detectSwitch(swState_t *ss)
// detects the HIGH to LOW transition of a switch
// returns true if a transition has occurred
// only check if a period of time has expired to debounce
{
boolean b = false;
if ((millis() - ss->lastCheckTime) > 50)
{
bool curState = (digitalRead(ss->pin) == LOW);
ss->lastCheckTime = millis();
b = (curState && !ss->lastState);
ss->lastState = curState;
}
return(b);
}
void userMessage(char *psz)
{
mp.clear(1, 0, mp.getXMax(), USER_MESG);
mp.drawText(1, USER_MESG, psz, MD_MAXPanel::ROT_0, true);
}
uint8_t getMove(void)
// get the next move from the player
// there may not be a move there so we need to split the
// function into a prompting and then checking phase
// return 0xff of no move entered
{
static enum { UI_START, UI_HILIGHT, UI_SELECT, UI_NEXT_HILIGHT, UI_ACCEPT } promptMode = UI_START;
static uint8_t curPosUI = 0;
uint8_t m = 0xff;
switch (promptMode)
{
case UI_START: // print the message
PRINTS("\n- START");
userMessage("You move");
promptMode = UI_HILIGHT;
break;
case UI_HILIGHT: // find the first empty cell and highlight it
PRINTS("\n- HILIGHT");
//highlightCell(curPosUI, false); // unhighlight current selection
for (curPosUI = 0; curPosUI<TTT_BOARD_SIZE; curPosUI++)
{
PRINT(" ", curPosUI);
if (TTT.getBoardPosition(curPosUI) == TTT_P0)
{
PRINTS(": FOUND");
highlightCell(curPosUI); // highlight new selection
break;
}
}
promptMode = UI_SELECT;
break;
case UI_SELECT: // highlight the cell we are on and handle switches
if (detectSwitch(&swSelect))
promptMode = UI_NEXT_HILIGHT;
else if (detectSwitch(&swAccept))
promptMode = UI_ACCEPT;
break;
case UI_NEXT_HILIGHT: // user selected next cell, find it and highlight it
PRINTS("\n- NEXT HILIGHT");
if (curPosUI < TTT_BOARD_SIZE)
highlightCell(curPosUI, false); // unhighlight current selection
for (curPosUI=curPosUI+1; curPosUI<TTT_BOARD_SIZE; curPosUI++)
{
PRINT(" ", curPosUI);
if (TTT.getBoardPosition(curPosUI) == TTT_P0)
{
PRINTS(": FOUND");
highlightCell(curPosUI); // highlight new selection
break;
}
}
if (curPosUI == TTT_BOARD_SIZE) // oops - none there
{
PRINTS(": NOT FOUND");
promptMode = UI_HILIGHT; // start again
}
else
promptMode = UI_SELECT; // wait for a switch again
break;
case UI_ACCEPT: // we have a selection, return the appropriate move
PRINTS("\n- ACCEPT");
highlightCell(curPosUI, false); // unhighlight current selection
m = curPosUI;
promptMode = UI_START; // set up for next time
break;
default: // catch all - reset
promptMode = UI_START;
break;
}
return(m);
}
void tttCallback(uint8_t position, int8_t player)
// update the board position with the player token
{
displayPosition(position, player);
}
void displayPosition(uint8_t pos, int8_t player)
{
// update the position on the grid
switch (player)
{
case TTT_P0: // clear the space
mp.clear(movePos[pos].x, movePos[pos].y,
movePos[pos].x+movePos[pos].size, movePos[pos].y-movePos[pos].size);
break;
case TTT_P1: // draw an X
mp.drawLine(movePos[pos].x+1, movePos[pos].y-1,
movePos[pos].x-1+movePos[pos].size, movePos[pos].y+1-movePos[pos].size);
mp.drawLine(movePos[pos].x+1, movePos[pos].y+1-movePos[pos].size,
movePos[pos].x-1+movePos[pos].size, movePos[pos].y-1);
break;
case TTT_P2: // draw an O
mp.drawCircle(movePos[pos].x+(movePos[pos].size / 2), movePos[pos].y-(movePos[pos].size / 2),
(movePos[pos].size / 2) - 1);
break;
}
}
void flashLine(uint8_t line)
// note this is blocking as it uses delay();
{
uint8_t l[3];
// work out the cells for this line
switch (line)
{
case TTT_WL_D1: l[0]=0; l[1]=4; l[2]=8; break;
case TTT_WL_D2: l[0]=2; l[1]=4; l[2]=6; break;
case TTT_WL_H1: l[0]=0; l[1]=1; l[2]=2; break;
case TTT_WL_H2: l[0]=3; l[1]=4; l[2]=5; break;
case TTT_WL_H3: l[0]=6; l[1]=7; l[2]=8; break;
case TTT_WL_V1: l[0]=0; l[1]=3; l[2]=6; break;
case TTT_WL_V2: l[0]=1; l[1]=4; l[2]=7; break;
case TTT_WL_V3: l[0]=2; l[1]=5; l[2]=8; break;
}
// turn them off and on a number of times (flash!)
for (uint8_t i=0; i<FLASH_REPEAT; i++)
{
mp.update(false);
for (uint8_t j=0; j<ARRAY_SIZE(l); j++)
displayPosition(l[j], TTT_P0);
mp.update(true);
delay(FLASH_DELAY);
mp.update(false);
for (uint8_t j=0; j<ARRAY_SIZE(l); j++)
displayPosition(l[j], TTT.getBoardPosition(l[j]));
mp.update(true);
delay(FLASH_DELAY);
}
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_TTT]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
//mp.setRotation(MD_MAXPanel::ROT_90);
// initialize switch pins for input
pinMode(swAccept.pin, INPUT_PULLUP);
pinMode(swSelect.pin, INPUT_PULLUP);
// set up global constants
USER_MESG = mp.getFontHeight() + 1;
GRID_BOTTOM = USER_MESG + 1;
GRID_RIGHT = mp.getXMax();
GRID_TOP = mp.getYMax();
// initialize display
calcGridSize();
TTT.setAutoPlayer(curPlayer);
}
void loop(void)
{
static enum { S_SPLASH, S_START, S_GET_MOVE, S_CHECK_END } curState = S_SPLASH; // current state
switch (curState)
{
case S_SPLASH: // show splash screen at start
{
const uint16_t border = 2;
PRINTSTATE("S_SPLASH");
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax() - border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - border, mp.getYMax() - border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight()) / 2, TITLE_TEXT);
delay(SPLASH_DELAY);
curState = S_START;
}
break;
case S_START: // initialize for a new game
PRINTSTATE("S_START");
displayGrid();
inGamePlay = TTT.start();
curState = S_GET_MOVE;
break;
case S_GET_MOVE: // get and make player move - this section is non-blocking
{
uint8_t m = 0;
if (TTT.getAutoPlayer() != curPlayer)
m = getMove();
if (m != 0xff)
{
TTT.doMove(m, curPlayer);
curState = S_CHECK_END;
}
}
break;
case S_CHECK_END: // switch players and check if this is the end of the game
PRINTSTATE("S_CHECK_END");
if (TTT.isGameOver())
{
inGamePlay = false;
if (TTT.getGameWinner() == TTT_P0)
{
userMessage("A draw");
delay(FLASH_REPEAT * 2 * FLASH_DELAY); // yes this blocks - so does flashLine()
}
else if (TTT.getGameWinner() == TTT.getAutoPlayer())
userMessage("I win!");
else
userMessage("You win!");
if (TTT.getGameWinner() != TTT_P0) // not a draw
flashLine(TTT.getWinLine());
curState = S_START; // restart
}
else
curState = S_GET_MOVE; // get or make next move
// switch turns for players
curPlayer = (curPlayer == TTT_P1 ? TTT_P2 : TTT_P1);
break;
default:
PRINTSTATE("DEFAULT!");
curState = S_START;
break;
}
}

View File

@@ -0,0 +1,15 @@
// Coordinates for the top left corner of the board cells on the display and size (square)
typedef struct
{
uint16_t x, y;
uint8_t size;
} boardCoord_t;
// Switch status
typedef struct
{
uint8_t pin;
bool lastState;
uint32_t lastCheckTime;
} swState_t;

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,442 @@
// Program to exercise the MD_MAXPanel library
//
// Uses most of the functions in the library
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
#include <MD_MAXPanel.h>
#include "Font5x3.h"
// Turn on debug statements to the serial output
#define DEBUG 1
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) Serial.print(F(x))
#define PRINTD(x) Serial.print(x, DEC)
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#endif
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
// We always wait a bit between updates of the display
#define DELAYTIME 100 // in milliseconds
void zeroPointer(void)
// Demonstrates the use of setPoint and
// show where the zero point is in the display
{
PRINTS("\nZero point highlight");
mp.clear();
for (uint8_t i=0; i<=max(mp.getXMax(), mp.getYMax()); i++)
{
mp.setPoint(i, i, true);
mp.setPoint(0, i, true);
mp.setPoint(i, 0, true);
delay(DELAYTIME/mp.getXMax());
}
delay(DELAYTIME*6);
}
void showUp(void)
// Triangle pointing to the of the display as currently rotated
{
PRINTS("\nTop of display");
char sz[] = "UP";
uint8_t w, h;
mp.setFont(_Fixed_5x3);
w = mp.getTextWidth(sz);
h = 5;
mp.clear();
mp.drawTriangle(0, mp.getYMax() / 2, (mp.getXMax() + 1) / 2, mp.getYMax(), mp.getXMax(), mp.getYMax() / 2, true);
mp.drawText((mp.getXMax() - w) / 2, (mp.getYMax() / 2) + 2 + h, sz);
mp.setFont(nullptr);
delay(DELAYTIME * 10);
}
void brightness(void)
// Demonstrate the use of setBrightness()
// Striped display so as not to overload power supply
{
const uint8_t SKIP_COUNT = 3;
uint8_t c = 0;
PRINTS("\nBrigtness");
mp.clear();
// set up diagonal dots on the display to avoid using too
// much power in big displays
for (uint16_t i = 0; i <= mp.getXMax(); i++)
{
uint8_t ctr = c;
c++;
if (c > SKIP_COUNT) c = 0;
for (uint16_t j = 0; j <= mp.getYMax(); j++)
{
mp.setPoint(i, j, (ctr == 0));
ctr++;
if (ctr > SKIP_COUNT) ctr = 0;
}
}
// now show the brightness
for (uint8_t i = 0; i < MAX_INTENSITY; i++)
{
mp.setIntensity(i);
PRINT(" ", i);
delay(DELAYTIME * 5);
}
// reset to a sensible value
mp.setIntensity(7);
}
void lines(void)
// Demonstrate the use of drawLine().
// Stepped fan out lines from each corner
{
PRINTS("\nLines");
const uint8_t stepSize = 4;
mp.clear();
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(0, 0, x, mp.getYMax(), true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(0, mp.getYMax(), x, 0, true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - x, 0, true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - x, mp.getYMax(), true);
delay(DELAYTIME);
mp.clear();
}
}
void hLines(void)
// Demonstrate the use of drawHLine()
// Draw a sawtooth across the display (depends on aspect ratio)
{
PRINTS("\nHorizontal Lines");
mp.clear();
for (uint8_t y = 0; y < mp.getYMax(); y++)
{
mp.drawHLine(y, 0, y % mp.getXMax(), true);
delay(DELAYTIME/4);
}
}
void vLines(void)
// Demonstrate the use of drawVLine()
// Stepped increment lines from zero point
{
PRINTS("\nVertical Lines");
mp.clear();
for (uint16_t x = 0; x < mp.getXMax(); x++)
{
mp.drawVLine(x, 0, x % mp.getYMax(), true);
delay(DELAYTIME/4);
}
}
void rectanglesFill(void)
// Demonstrate the use of drawFillRectangle()
// Various sized rectangles spanning the entire display
{
PRINTS("\nRectangles Fill");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawFillRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, true);
delay(2 * DELAYTIME);
mp.drawFillRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, false);
delay(2 * DELAYTIME);
}
}
}
void rectangles(void)
// Demonstrate the use of drawRectangle()
// Nested rectangles spanning the entire display
{
PRINTS("\nRectangles");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, true);
delay(2 * DELAYTIME);
}
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, false);
delay(2 * DELAYTIME);
}
}
}
void quadrilaterals(void)
// Demonstrate the use of drawQuadrilateral()
// Rubber band quadrilaterals anchored to the display edge
{
const uint8_t numQuadri = min(mp.getXMax(), mp.getYMax())/2;
const uint8_t stepSizeX = mp.getXMax() / numQuadri;
const uint8_t stepSizeY = mp.getYMax() / numQuadri;
const uint8_t offset = 2;
PRINTS("\nQuadrilaterals");
mp.clear();
for (uint8_t j = 0; j < 4; j++)
{
for (uint16_t i = 0; i < numQuadri; i++)
{
uint16_t x = stepSizeX * i;
uint16_t y = stepSizeY * i;
mp.drawQuadrilateral(x+offset, 0+offset, 0+offset, mp.getYMax() - y - offset, mp.getXMax() - x - offset, mp.getYMax() - offset, mp.getXMax() - offset, y + offset, true);
delay(DELAYTIME/2);
mp.drawQuadrilateral(x + offset, 0 + offset, 0 + offset, mp.getYMax() - y - offset, mp.getXMax() - x - offset, mp.getYMax() - offset, mp.getXMax() - offset, y + offset, false);
}
}
}
void trianglesFill(void)
// Demonstrate the use of drawFillTriangle()
// Random triangles.
{
const uint8_t NUM_TRIANGLE = 25;
const uint8_t NUM_VERTEX = 3;
uint16_t x[NUM_VERTEX], y[NUM_VERTEX];
PRINTS("\nTriangles Fill");
mp.clear();
for (uint8_t j = 0; j < NUM_TRIANGLE; j++)
{
for (uint16_t i = 0; i < NUM_VERTEX; i++)
{
x[i] = random(mp.getXMax() + 1);
y[i] = random(mp.getYMax() + 1);
}
mp.drawFillTriangle(x[0], y[0], x[1], y[1], x[2], y[2], true);
delay(2 * DELAYTIME);
mp.drawFillTriangle(x[0], y[0], x[1], y[1], x[2], y[2], false);
delay(2 * DELAYTIME);
}
}
void triangles(void)
// Demonstrate the use of drawTriangle()
// Random triangles.
{
const uint8_t NUM_TRIANGLE = 25;
const uint8_t NUM_VERTEX = 3;
uint16_t x[NUM_VERTEX], y[NUM_VERTEX];
PRINTS("\nTriangles");
mp.clear();
for (uint8_t j = 0; j < NUM_TRIANGLE; j++)
{
for (uint16_t i = 0; i < NUM_VERTEX; i++)
{
x[i] = random(mp.getXMax() + 1);
y[i] = random(mp.getYMax() + 1);
}
mp.drawTriangle(x[0], y[0], x[1], y[1], x[2], y[2], true);
delay(2 * DELAYTIME);
mp.drawTriangle(x[0], y[0], x[1], y[1], x[2], y[2], false);
delay(2 * DELAYTIME);
}
}
void circlesFill(void)
// Demonstrate the use of drawFillCircle()
// Nested circles spanning the entire display
{
PRINTS("\nCircles Fill");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawFillCircle(mp.getXMax()/2, mp.getYMax()/2, i, true);
delay(2 * DELAYTIME);
mp.drawFillCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, false);
delay(2 * DELAYTIME);
}
}
}
void circles(void)
// Demonstrate the use of drawCircle()
// Nested circles spanning the entire display
{
PRINTS("\nCircles");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, true);
delay(2 * DELAYTIME);
}
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, false);
delay(2 * DELAYTIME);
}
}
}
void bounce(void)
// Animation of a bouncing ball
{
const uint16_t minX = 0;
const uint16_t maxX = mp.getXMax();
const uint16_t minY = 0;
const uint16_t maxY = mp.getYMax();
uint16_t nCounter = 0;
uint16_t y = random(maxY/2) + maxY/2, x = random(maxX/2) + maxX/2;
int8_t dy = 1, dx = 1; // delta row and column
PRINTS("\nBouncing ball");
mp.clear();
while (nCounter++ < 200)
{
mp.setPoint(x, y, false);
x += dx;
y += dy;
mp.setPoint(x, y, true);
delay(DELAYTIME/2);
if ((x == minX) || (x == maxX))
dx = -dx;
if ((y == minY) || (y == maxY))
dy = -dy;
}
}
void text(MD_MAX72XX::fontType_t *fontData)
// Demonstrate the use of drawText()
// Display text in different orientations and fonts
{
PRINTS("\nText");
mp.setFont(fontData);
PRINT(" - Font height: ", mp.getFontHeight());
mp.clear();
mp.drawText(0, mp.getYMax(), "R_0", MD_MAXPanel::ROT_0);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(0, 0, "R_90", MD_MAXPanel::ROT_90);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(mp.getXMax(), 0, "R_180", MD_MAXPanel::ROT_180);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(mp.getXMax(), mp.getYMax(), "R_270", MD_MAXPanel::ROT_270);
delay(5 * DELAYTIME);
}
void setup(void)
{
mp.begin();
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel Test & Demo]");
}
void loop(void)
{
zeroPointer();
showUp();
brightness();
lines();
hLines();
vLines();
rectangles();
rectanglesFill();
circles();
circlesFill();
triangles();
trianglesFill();
quadrilaterals();
bounce();
text(_Fixed_5x3);
text(nullptr);
// rotate the display and do it all again
mp.setRotation(mp.getRotation() == MD_MAXPanel::ROT_0 ? MD_MAXPanel::ROT_90 : MD_MAXPanel::ROT_0);
}

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,694 @@
// Implements the game of Tetris using MD_MAXPanel library
// Tips for coding this taken from https://www.youtube.com/watch?v=8OK8_tHeCIA
//
// Hardware used
// =============
//
// LEFT_PIN - move left switch, INPUT_PULLUP
// RIGHT_PIN - move right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - drop the piece switch, INPUT_PULLUP
// SELECT_PIN - rotate the piece switch, INPUT_PULLUP
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Game pieces are shaped like tetrominoes, geometric shapes composed of four
// square blocks each. A random sequence of tetrominoes fall down the playing
// field (a rectangular vertical shaft, called the "well" or "matrix"). The
// objective of the game is to manipulate these tetrominoes, by moving each one
// sideways (if the player feels the need) and rotating it by 90 degree units,
// with the aim of creating a horizontal line of ten units without gaps. When
// such a line is created, it gets destroyed, and any block above the deleted
// line will fall. When a certain number of lines are cleared, the game enters
// a new level. As the game progresses, each level causes the tetrominoes to fall
// faster, and the game ends when the stack of tetrominoes reaches the top of
// the playing field and no new tetrominos are able to enter.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
const uint16_t FIELD_WIDTH = 10; // playable area width inside 'bucket'
const uint16_t FIELD_HEIGHT = 21; // playable area depth inside 'bucket'
const uint16_t FIELD_BOTTOM = 3; // y coord bottom wall
const uint16_t FIELD_LEFT = 5; // x coord left wall
const uint16_t FIELD_TOP = FIELD_BOTTOM + FIELD_HEIGHT; // y coord top opening of the 'bucket'
const uint16_t FIELD_RIGHT = FIELD_LEFT + FIELD_WIDTH + 1; // x coord right wall
const char TITLE_TEXT[] = "TETRIS";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = 60000;
const uint16_t PIECE_SCORE = 5;
const uint16_t LINE_SCORE = 20;
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
// A class to encapsulate the snake direction switches
// Can move up, down, rotate, drop
class cMoveSW
{
private:
uint8_t _pinLeft;
uint8_t _pinRight;
uint8_t _pinRotate;
uint8_t _pinDrop;
uint16_t _timeDelay; // the delay between switch detection
uint32_t _timeLastCheck; // the millis() value for the last time we checked
public:
enum moveType_t { MOVE_NONE, MOVE_LEFT, MOVE_RIGHT, MOVE_DROP, MOVE_ROTATE };
void begin(uint8_t pinL, uint8_t pinR, uint8_t pinRot, uint8_t pinD)
{
_timeDelay = 100;
_pinLeft = pinL;
_pinRight = pinR;
_pinRotate = pinRot;
_pinDrop = pinD;
_timeLastCheck = 0;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
pinMode(_pinRotate, INPUT_PULLUP);
pinMode(_pinDrop, INPUT_PULLUP);
}
bool anyKey(void) { return (move() != MOVE_NONE);}
moveType_t move(void)
{
if (millis() - _timeLastCheck < _timeDelay)
return(MOVE_NONE);
_timeLastCheck = millis();
if (digitalRead(_pinLeft) == LOW)
return(MOVE_LEFT);
else if (digitalRead(_pinRight) == LOW)
return(MOVE_RIGHT);
else if (digitalRead(_pinRotate) == LOW)
return(MOVE_ROTATE);
else if (digitalRead(_pinDrop) == LOW)
return(MOVE_DROP);
return(MOVE_NONE);
}
};
// A class to encapsulate the Tetris field
class cTetris
{
private:
const uint8_t OMINO_SIZE = 4; // 4x4 field flattened out into 16 bits
const uint8_t MAX_ROTATE = 4; // maximum number of rotations
uint16_t _tetromino[7]; // tetronimoes, values initialized in the constructor
bool _field[FIELD_WIDTH][FIELD_HEIGHT];
uint32_t _timeLastMove; // last time the snake was moved
uint16_t _moveDelay; // the delay between moves in milliseconds
bool _run; // game is playing when true
int8_t _pieceCount; // number of pieces completed
uint8_t _curOmino; // current tetronimo
uint8_t _nxtOmino; // the next tetronimo
uint8_t _curRotation; // current rotated orientation
uint16_t _x, _y; // _field coordinates for the piece
cScore *_pScore; // for keeping score
cSound *_pSound; // for making noise
void dumpOmino(uint8_t omino, uint8_t rot)
// for debugging only
{
PRINT("\n -- DUMPOMINO ", omino); PRINT(" R:", rot);
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
PRINT("\n L", j); PRINTS(": ");
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// draw the point
if ((_tetromino[omino] >> idx) & 1) { PRINTS(" 1") }
else { PRINTS(" 0") }
}
}
}
void showOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y, bool b)
// show the tetronimo on the actual display
// x and y need to be display coordinates
{
//PRINT("\n-- SHOW ", omino);
//PRINTXY(" @", x, y);
//PRINT(" b=", b);
mp.update(false);
//dumpOmino(omino, rot);
for (int8_t j = 0; j < OMINO_SIZE; j++)
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// draw the point
//PRINTXY(" ", FIELD_LEFT + x + i, FIELD_TOP - y - j);
//PRINT(" i", idx);
//PRINT(" v", (_tetromino[omino] >> idx) & 1);
if ((_tetromino[omino] >> idx) & 1)
mp.setPoint(x + i, y - j, b);
}
mp.update(true);
}
void drawNxtOmino(void) { showOmino(_nxtOmino, 0, FIELD_RIGHT + OMINO_SIZE, FIELD_TOP - (2*OMINO_SIZE), true); }
void eraseNxtOmino(void) { showOmino(_nxtOmino, 0, FIELD_RIGHT + OMINO_SIZE, FIELD_TOP - (2*OMINO_SIZE), false); }
void drawOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y) { showOmino(omino, rot, FIELD_LEFT + x + 1, FIELD_TOP - y, true); }
void eraseOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y) { showOmino(omino, rot, FIELD_LEFT + x + 1, FIELD_TOP - y, false); }
void omino2Field(uint8_t omino, uint8_t rot, int16_t x, int16_t y)
// copy the tetronimo to the field, leaving the rest untouched
{
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// set in field if set in tetronimo
if ((_tetromino[omino] >> idx) & 1)
_field[x + i][y + j] = true;
}
}
}
void displayField(void)
// display the field, mindful of offsets in displayable field
{
mp.update(false);
for (uint8_t j = 0; j < FIELD_HEIGHT; j++)
for (uint8_t i = 0; i < FIELD_WIDTH; i++)
mp.setPoint(FIELD_LEFT+i+1, FIELD_TOP-j, _field[i][j]);
mp.update(true);
}
void clearField(void)
// clear the playing field
{
memset(_field, 0, sizeof(bool)*FIELD_HEIGHT*FIELD_WIDTH);
displayField();
}
uint8_t rotate(uint8_t x, uint8_t y, uint8_t r)
// return the linear index for from the x, y coords and the
// current rotation
{
uint8_t idx = 0;
switch (r) // Rotation effect
{
case 0: // 0 degrees // 0 1 2 3
idx = y * 4 + x; // 4 5 6 7
break; // 8 9 10 11
//12 13 14 15
case 1: // 90 degrees //12 8 4 0
idx = 12 + y - (x * 4); //13 9 5 1
break; //14 10 6 2
//15 11 7 3
case 2: // 180 degrees //15 14 13 12
idx = 15 - (y * 4) - x; //11 10 9 8
break; // 7 6 5 4
// 3 2 1 0
case 3: // 270 degrees // 3 7 11 15
idx = 3 - y + (x * 4); // 2 6 10 14
break; // 1 5 9 13
} // 0 4 8 12
return(idx);
}
bool checkPieceFit(uint8_t omino, uint8_t rot, int16_t x, int16_t y)
// does the piece fit in the _field array?
{
/*
PRINTXY("\n-- CHKFIT @", x, y);
PRINT(" T:", omino);
PRINT(" R:", rot);
PRINT(" H:", FIELD_HEIGHT);
PRINT(" W:", FIELD_WIDTH);
*/
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
bool bCell = (_tetromino[omino] >> idx) & 1;
if (bCell) // we have an occupied cell in the tetromino
{
// PRINTXY("\n", i, j); PRINTXY("=>", x + i, y + j); PRINT(" i:", idx); PRINT(" cell: ", bCell);
// check it is in bounds
if (x + i < 0 || x + i >= FIELD_WIDTH)
{
//PRINTS(" fail x");
return(false); // out of side bounds
}
else if (y + j < 0 || y + j >= FIELD_HEIGHT)
{
//PRINTS(" fail y");
return(false); // out of bounds
}
else if (_field[x + i][y + j]) // in bounds - do a collision check
{
//PRINTS(" fail field");
return(false); // occupied field cell
}
}
}
}
//PRINTS(" pass");
return(true);
}
public:
enum moveType_t { M_LEFT, M_RIGHT, M_DROP, M_ROTATE };
cTetris(void)
{
// straight block
// 0010
// 0010
// 0010
// 0010
_tetromino[0] = 0x2222;
// T block
// 0010
// 0110
// 0010
// 0000
_tetromino[1] = 0x2620;
// square block
// 0000
// 0110
// 0110
// 0000
_tetromino[2] = 0x0660;
// normal Z block
// 0010
// 0110
// 0100
// 0000
_tetromino[3] = 0x2640;
// reversed Z block
// 0100
// 0110
// 0010
// 0000
_tetromino[4] = 0x4620;
// normal L block
// 0100
// 0100
// 0110
_tetromino[5] = 0x4460;
// reversed L block
// 0010
// 0010
// 0110
_tetromino[6] = 0x2260;
}
uint16_t getDelay(void) { return (_moveDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _moveDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void begin(cScore *pScore, cSound *pSound)
{
_timeLastMove = 0;
_moveDelay = 1000;
_pieceCount = 0;
_run = false;
// set up the next tetronimo
_curOmino = 0;
_nxtOmino = random(ARRAY_SIZE(_tetromino));
_curRotation = 0;
// save and reset the score
_pScore = pScore;
_pScore->reset();
// save the sound object
_pSound = pSound;
// clear and display the field
clearField();
}
bool nextOmino(void)
{
_x = (FIELD_WIDTH - OMINO_SIZE) / 2;
_y = 0;
_curRotation = 0;
_curOmino = _nxtOmino;
if (!checkPieceFit(_curOmino, _curRotation, _x, _y))
return(false); // can't fit it it, pass the message back!
drawOmino(_curOmino, _curRotation, _x, _y);
_pSound->hit();
// work out and display the next omino
eraseNxtOmino();
_nxtOmino = random(ARRAY_SIZE(_tetromino));
drawNxtOmino();
return(true);
}
bool move(moveType_t moveType)
// user defined action to do something
{
bool b = false;
eraseOmino(_curOmino, _curRotation, _x, _y);
switch (moveType)
{
case M_LEFT:
if (b = checkPieceFit(_curOmino, _curRotation, _x - 1, _y))
_x--;
break;
case M_RIGHT:
if (b = checkPieceFit(_curOmino, _curRotation, _x + 1, _y))
_x++;
break;
case M_ROTATE:
if (b = checkPieceFit(_curOmino, (_curRotation + 1) % MAX_ROTATE, _x, _y))
_curRotation = (_curRotation + 1) % MAX_ROTATE;
break;
case M_DROP:
if (b = checkPieceFit(_curOmino, _curRotation, _x, _y + 1))
_y++;
break;
}
drawOmino(_curOmino, _curRotation, _x, _y);
return(b);
}
bool run(void)
// return false if the game has ended
{
bool bReturn = true;
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _moveDelay)
return(true);
_timeLastMove = millis();
// check if the piece can be moved down
if (checkPieceFit(_curOmino, _curRotation, _x, _y + 1))
{
// yes - do it and animate the display
eraseOmino(_curOmino, _curRotation, _x, _y);
_y++;
drawOmino(_curOmino, _curRotation, _x, _y);
// now adjust the seed if we have reached the threshold
if (_pieceCount >= 10)
{
// cap how fast we make it!
if (_moveDelay >= 200) _moveDelay -= 50;
_pieceCount = 0;
}
}
else
{
_pieceCount++; // just landed another one
// insert the current piece in the field, show it and update the score
omino2Field(_curOmino, _curRotation, _x, _y);
displayField();
_pScore->increment(PIECE_SCORE);
// Check for lines to delete
// There can only be lines in the span of the last block, so
// check the 4 lines it takes up
uint16_t lines = 0;
for (int16_t y = _y + OMINO_SIZE; (y >= _y) && (y >= 0); y--)
{
int16_t count = 0;
for (int16_t x = 0; x < FIELD_WIDTH; x++)
count += (_field[x][y] ? 1 : 0);
if (count == FIELD_WIDTH)
{
lines++; // keep count of lines completed
// delete the line - blank it out, pause for effect,
// make a sound then collapse the lines!
for (int16_t x = 0; x < FIELD_WIDTH; x++)
_field[x][y] = false;
displayField();
delay(200);
_pSound->bounce();
for (int16_t j = y; j >= 1; j--)
for (int16_t x = 0; x < FIELD_WIDTH; x++)
_field[x][j] = _field[x][j-1];
displayField();
// roll back the index as we have just changed the lines
y++;
}
}
// update the score if deleted lines
if (lines != 0) _pScore->increment((1 << lines)*LINE_SCORE);
// create the next omino and return status
bReturn = nextOmino();
}
return(bReturn);
}
};
// main objects coordinated by the code logic
cScore score;
cMoveSW moveSW;
cSound sound;
cTetris tetris;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_BOTTOM, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, FIELD_TOP, FIELD_BOTTOM);
mp.drawVLine(FIELD_RIGHT, FIELD_TOP, FIELD_BOTTOM);
score.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Tetris]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
// mp.setRotation(MD_MAXPanel::ROT_90);
sound.begin(BEEPER_PIN);
score.limit(MAX_SCORE); // so we can use width() below
score.begin(&mp, mp.getXMax() - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), mp.getYMax() - 1, MAX_SCORE);
moveSW.begin(LEFT_PIN, RIGHT_PIN, SELECT_PIN, DOWN_PIN);
randomSeed(seedOut(31, RANDOM_SEED_PORT));
}
bool handleUI(void)
{
bool b = true;
switch (moveSW.move())
{
case cMoveSW::MOVE_ROTATE:tetris.move(cTetris::M_ROTATE); break;
case cMoveSW::MOVE_DROP: tetris.move(cTetris::M_DROP); break;
case cMoveSW::MOVE_LEFT: tetris.move(cTetris::M_LEFT); break;
case cMoveSW::MOVE_RIGHT: tetris.move(cTetris::M_RIGHT); break;
case cMoveSW::MOVE_NONE: b = false; break;
}
return(b);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
setupField();
tetris.begin(&score, &sound);
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (moveSW.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
tetris.start();
if (tetris.nextOmino())
{
runState = S_PLAY;
PRINTSTATE("PLAY");
}
else
runState = S_GAME_OVER;
}
break;
case S_PLAY: // playing a point
// handle the switches
handleUI();
// move and finish the game if false
if (!tetris.run())
{
tetris.stop();
runState = S_GAME_OVER;
}
break;
case S_GAME_OVER:
{
uint16_t w, x, y;
PRINTSTATE("GAME_OVER");
w = mp.getTextWidth(GAME_TEXT);
x = ((mp.getXMax() - w) / 2) - 1;
y = (mp.getYMax() / 2) + mp.getFontHeight() + 2;
mp.clear(x, y, x + w + 2, y - mp.getFontHeight() - 2);
mp.drawText(x + 1, y - 1, GAME_TEXT);
w = mp.getTextWidth(OVER_TEXT);
x = ((mp.getXMax() - w) / 2) - 1;
y = (mp.getYMax() / 2) - 1 + 2;
mp.clear(x, y, x + w + 2, y - mp.getFontHeight() - 1);
mp.drawText(x + 1, y - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
}
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,51 @@
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
MD_MAXPanel KEYWORD1
rotation_t KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
clear KEYWORD2
setPoint KEYWORD2
getPoint KEYWORD2
drawLine KEYWORD2
drawHLine KEYWORD2
drawVLine KEYWORD2
drawRecangle KEYWORD2
drawFillRectangle KEYWORD2
drawTriangle KEYWORD2
drawFillTriangle KEYWORD2
drawCircle KEYWORD2
drawFillCircle KEYWORD2
drawQuadrilateral KEYWORD2
getXMax KEYWORD2
getYMax KEYWORD2
getGraphicObject KEYWORD2
update KEYWORD2
setIntensity KEYWORD2
setFont KEYWORD2
setCharSpacing KEYWORD2
getCharSpacing KEYWORD2
setRotation KEYWORD2
getRotation KEYWORD2
getTextWidth KEYWORD2
getFontHeight KEYWORD2
drawText KEYWORD2
######################################
# Constants (LITERAL1)
#######################################
ROT_0 LITERAL1
ROT_90 LITERAL1
ROT_180 LITERAL1
ROT_270 LITERAL1

View File

@@ -0,0 +1,11 @@
name=MD_MAXPanel
version=1.2.3
author=majicDesigns
maintainer=marco_c <8136821@gmail.com>
sentence=Implements functions to manage a panel of MAX72xx based LED modules
paragraph=Allows the programmer to use the LED matrix panel as a pixel addressable display for graphics and text.
category=Device Control
url=https://github.com/MajicDesigns/MD_MAXPanel
architectures=*
includes=MD_MAXPanel.h
license=LGPL-2.1

View File

@@ -0,0 +1,660 @@
/*
MD_MAXPanel - Library for MAX7219/7221 LED Panel
See header file for comments
This file contains class and hardware related methods.
Copyright (C) 2018 Marco Colli. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <Arduino.h>
#include "MD_MAXPanel.h"
#include "MD_MAXPanel_lib.h"
/**
* \file
* \brief Implements class graphics methods
*/
MD_MAXPanel::MD_MAXPanel(MD_MAX72XX::moduleType_t mod, uint8_t dataPin, uint8_t clkPin, uint8_t csPin, uint8_t xDevices, uint8_t yDevices) :
_xDevices(xDevices), _yDevices(yDevices), _rotatedDisplay(false)
{
_D = new MD_MAX72XX(mod, dataPin, clkPin, csPin, xDevices*yDevices);
_killOnDestruct = true;
}
MD_MAXPanel::MD_MAXPanel(MD_MAX72XX::moduleType_t mod, uint8_t csPin, uint8_t xDevices, uint8_t yDevices) :
_xDevices(xDevices), _yDevices(yDevices), _rotatedDisplay(false)
{
_D = new MD_MAX72XX(mod, csPin, xDevices*yDevices);
_killOnDestruct = true;
}
MD_MAXPanel::MD_MAXPanel(MD_MAX72XX *D, uint8_t xDevices, uint8_t yDevices) :
_xDevices(xDevices), _yDevices(yDevices), _rotatedDisplay(false)
{
_D = D;
_killOnDestruct = false;
}
void MD_MAXPanel::begin(void)
{
_D->begin();
_charSpacing = CHAR_SPACING_DEFAULT;
_updateEnabled = true;
}
MD_MAXPanel::~MD_MAXPanel(void)
{
if (_killOnDestruct) delete _D;
}
uint16_t MD_MAXPanel::getXMax(void)
{
uint16_t m;
if (_rotatedDisplay)
m = (_yDevices * ROW_SIZE) - 1;
else
m = (_xDevices * COL_SIZE) - 1;
return(m);
}
uint16_t MD_MAXPanel::getYMax(void)
{
uint16_t m;
if (_rotatedDisplay)
m = (_xDevices * COL_SIZE) - 1;
else
m = (_yDevices * ROW_SIZE) - 1;
return(m);
}
bool MD_MAXPanel::drawHLine(uint16_t y, uint16_t x1, uint16_t x2, bool state)
// draw a horizontal line at row y between columns x1 and x2 inclusive
{
bool b = true;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
if (x1 > x2) // swap x1/x2
{
uint16_t t = x1;
x1 = x2;
x2 = t;
}
for (uint16_t i = x1; i <= x2; i++)
b &= setPoint(i, y, state);
update(_updateEnabled);
return(b);
}
bool MD_MAXPanel::drawVLine(uint16_t x, uint16_t y1, uint16_t y2, bool state)
// draw a vertical line at column x between rows y1 and y2 inclusive
{
bool b = true;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
if (y1 > y2) // swap y1/y2
{
uint8_t t = y1;
y1 = y2;
y2 = t;
}
for (uint8_t i = y1; i <= y2; i++)
b &= setPoint(x, i, state);
update(_updateEnabled);
return(b);
}
bool MD_MAXPanel::drawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state)
// draw an arbitrary line between two points using Bresentham's line algorithm
// Bresentham's line algorithm at https://rosettacode.org/wiki/Bitmap/Bresenham%27s_line_algorithm#C
{
bool b = true;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
PRINT("\n\nLine from ", x1); PRINT(",", y1);
PRINT(" to ", x2); PRINT(",", y2);
if (x1 > x2) // swap direction for line
{
uint16_t t;
t = x1; x1 = x2; x2 = t;
t = y1; y1 = y2; y2 = t;
// PRINTS(" SWAP X");
}
// PRINT("\nPlotting from ", x1); PRINT(",", y1);
// PRINT(" to ", x2); PRINT(",", y2);
int16_t dx = x2 - x1;
int16_t sx = 1;
int16_t dy = y2 - y1;
if (dy < 0) dy = -dy;
int16_t sy = y1 < y2 ? 1 : -1;
int16_t err = (dx > dy ? dx : -dy) / 2;
int16_t e2;
// PRINT("\ndx=", dx);
// PRINT(" dy=", dy);
// PRINT(" ystep=", sy);
// PRINT(" xstep=", sx);
for (;;)
{
// PRINT("\nerror=", err);
// PRINT(" [", x1); PRINT(",", y1); PRINTS("]");
b &= setPoint(x1, y1, state);
if (x1 == x2 && y1 == y2) break;
e2 = err;
if (e2 >-dx) { err -= dy; x1 += sx; }
if (e2 < dy) { err += dx; y1 += sy; }
}
update(_updateEnabled);
return(b);
}
bool MD_MAXPanel::drawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state)
// draw a rectangle given the 2 diagonal vertices
{
bool b = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
b &= drawHLine(y1, x1, x2, state);
b &= drawHLine(y2, x1, x2, state);
b &= drawVLine(x1, y1, y2, state);
b &= drawVLine(x2, y1, y2, state);
update(u);
return(b);
}
bool MD_MAXPanel::drawFillRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state)
{
bool b = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
for (uint8_t i = x1; i <= x2; i++)
drawVLine(i, y1, y2, state);
update(u);
return(b);
};
bool MD_MAXPanel::drawQuadrilateral(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, uint16_t x4, uint16_t y4, bool state)
// draw a arbitrary quadrilateral given the 4 corner vertices
{
bool b = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
b &= drawLine(x1, y1, x2, y2, state);
b &= drawLine(x2, y2, x3, y3, state);
b &= drawLine(x3, y3, x4, y4, state);
b &= drawLine(x4, y4, x1, y1, state);
update(u);
return(b);
}
bool MD_MAXPanel::drawTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, bool state)
// draw an arbitrary triangle given the 3 corner vertices
{
bool b = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
b &= drawLine(x1, y1, x2, y2, state);
b &= drawLine(x2, y2, x3, y3, state);
b &= drawLine(x3, y3, x1, y1, state);
update(u);
return(b);
}
/*
bool MD_MAXPanel::drawFillTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, bool state)
// draw an arbitrary filled triangle given the 3 corner vertices
// Uses slope method, originally from Adafruit Industries
{
#define SWAP(a, b) { uint16_t t = a; a = b; b = t; }
uint8_t a, b, y, last;
bool r = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
// Sort coordinates by Y order (y3 >= y2 >= y1)
if (y1 > y2) { SWAP(y1, y2); SWAP(x1, x2); }
if (y2 > y3) { SWAP(y3, y2); SWAP(x3, x2); }
if (y1 > y2) { SWAP(y1, y2); SWAP(x1, x2); }
if (y1 == y3) // All in same line
{
a = b = x1;
if (x2 < a) a = x2;
else if (x2 > b) b = x2;
if (x3 < a) a = x3;
else if (x3 > b) b = x3;
r &= drawHLine(y1, a, b, state);
}
else
{
int8_t dx12 = x2 - x1;
int8_t dy12 = y2 - y1;
int8_t dx13 = x3 - x1;
int8_t dy13 = y3 - y1;
int8_t dx23 = x3 - x2;
int8_t dy23 = y3 - y2;
int16_t sa = 0, sb = 0;
// For upper part of triangle, find scanline crossings for segment
// 1-2 and 1-3. If y2=y3 (flat-bottomed triangle), the scanline y
// is included here (and second loop will be skipped, avoiding a /0
// error there), otherwise scanline y2 is skipped here and handle
// in the second loop...which also avoids a /0 error here if y1=y
// (flat-topped triangle)
if (y2 == y3) last = y2; // Include y2 scanline
else last = y2 - 1; // Skip it
for (y = y1; y <= last; y++)
{
a = x1 + sa / dy12;
b = x1 + sb / dy13;
sa += dx12;
sb += dx13;
// longhand a = x1 + (x2 - x1) * (y - y1) / (y2 - y1)
// b = x0 + (x3 - x1) * (y - y1) / (y3 - y1)
r &= drawHLine(y, a, b, state);
}
// For lower part of triangle, find scanline crossings for segment
// 1-3 and 2-3. This loop is skipped if y2=y3
sa = dx23 * (y - y2);
sb = dx13 * (y - y1);
for (; y <= y3; y++)
{
a = x2 + sa / dy23;
b = x1 + sb / dy13;
sa += dx23;
sb += dx13;
// longhand a = x2 + (x3 - x2) * (y - y2) / (y3 - y2)
// b = x1 + (x2 - x1) * (y - y1) / (y3 - y1)
r &= drawHLine(y, a, b, state);
}
}
update(u);
return(r);
}
*/
bool MD_MAXPanel::drawFillTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, bool state)
// Fill a triangle - Bresenham method
// Original from http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html
{
#define SWAP(a, b) { uint16_t t = a; a = b; b = t; }
uint8_t t1x, t2x, y, minx, maxx, t1xp, t2xp;
bool changed1 = false;
bool changed2 = false;
int8_t signx1, signx2, dx1, dy1, dx2, dy2;
uint8_t e1, e2;
bool b = true;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
// Sort vertices
if (y1>y2) { SWAP(y1, y2); SWAP(x1, x2); }
if (y1>y3) { SWAP(y1, y3); SWAP(x1, x3); }
if (y2>y3) { SWAP(y2, y3); SWAP(x2, x3); }
t1x = t2x = x1; y = y1; // Starting points
dx1 = (int8_t)(x2 - x1);
if (dx1<0) { dx1 = -dx1; signx1 = -1; }
else signx1 = 1;
dy1 = (int8_t)(y2 - y1);
dx2 = (int8_t)(x3 - x1);
if (dx2<0) { dx2 = -dx2; signx2 = -1; }
else signx2 = 1;
dy2 = (int8_t)(y3 - y1);
if (dy1 > dx1) // swap values
{
SWAP(dx1, dy1);
changed1 = true;
}
if (dy2 > dx2) // swap values
{
SWAP(dy2, dx2);
changed2 = true;
}
e2 = (uint8_t)(dx2 >> 1);
// Flat top, just process the second half
if (y1 == y2) goto next;
e1 = (uint8_t)(dx1 >> 1);
for (uint8_t i = 0; i < dx1;)
{
t1xp = 0; t2xp = 0;
if (t1x<t2x) { minx = t1x; maxx = t2x; }
else { minx = t2x; maxx = t1x; }
// process first line until y value is about to change
while (i<dx1)
{
i++;
e1 += dy1;
while (e1 >= dx1)
{
e1 -= dx1;
if (changed1) t1xp = signx1;//t1x += signx1;
else goto next1;
}
if (changed1) break;
else t1x += signx1;
}
// Move line
next1:
// process second line until y value is about to change
while (1)
{
e2 += dy2;
while (e2 >= dx2)
{
e2 -= dx2;
if (changed2) t2xp = signx2;//t2x += signx2;
else goto next2;
}
if (changed2) break;
else t2x += signx2;
}
next2:
if (minx>t1x) minx = t1x;
if (minx>t2x) minx = t2x;
if (maxx<t1x) maxx = t1x;
if (maxx<t2x) maxx = t2x;
b &= drawHLine(y, minx, maxx, state); // Draw line from min to max points found on the y
// Now increase y
if (!changed1) t1x += signx1;
t1x += t1xp;
if (!changed2) t2x += signx2;
t2x += t2xp;
y += 1;
if (y == y2) break;
}
next:
// Second half
dx1 = (int8_t)(x3 - x2);
if (dx1<0) { dx1 = -dx1; signx1 = -1; }
else signx1 = 1;
dy1 = (int8_t)(y3 - y2);
t1x = x2;
if (dy1 > dx1)
{ // swap values
SWAP(dy1, dx1);
changed1 = true;
}
else changed1 = false;
e1 = (uint8_t)(dx1 >> 1);
for (uint8_t i = 0; i <= dx1; i++)
{
t1xp = 0; t2xp = 0;
if (t1x<t2x) { minx = t1x; maxx = t2x; }
else { minx = t2x; maxx = t1x; }
// process first line until y value is about to change
while (i<dx1)
{
e1 += dy1;
while (e1 >= dx1)
{
e1 -= dx1;
if (changed1) { t1xp = signx1; break; }//t1x += signx1;
else goto next3;
}
if (changed1) break;
else t1x += signx1;
if (i<dx1) i++;
}
next3:
// process second line until y value is about to change
while (t2x != x3)
{
e2 += dy2;
while (e2 >= dx2)
{
e2 -= dx2;
if (changed2) t2xp = signx2;
else goto next4;
}
if (changed2) break;
else t2x += signx2;
}
next4:
if (minx>t1x) minx = t1x;
if (minx>t2x) minx = t2x;
if (maxx<t1x) maxx = t1x;
if (maxx<t2x) maxx = t2x;
b &= drawHLine(y, minx, maxx, state); // Draw line from min to max points found on the y
// Now increase y
if (!changed1) t1x += signx1;
t1x += t1xp;
if (!changed2) t2x += signx2;
t2x += t2xp;
y += 1;
if (y>y3) goto outtahere;
}
outtahere:
update(u);
return(b);
}
bool MD_MAXPanel::drawCirclePoints(uint16_t xc, uint16_t yc, uint16_t x, uint16_t y, bool state)
// draw symmetrical circle points
{
bool b = true;
b &= setPoint(xc + x, yc + y, state);
b &= setPoint(xc - x, yc + y, state);
b &= setPoint(xc + x, yc - y, state);
b &= setPoint(xc - x, yc - y, state);
b &= setPoint(xc + y, yc + x, state);
b &= setPoint(xc - y, yc + x, state);
b &= setPoint(xc + y, yc - x, state);
b &= setPoint(xc - y, yc - x, state);
return(b);
}
bool MD_MAXPanel::drawCircle(uint16_t xc, uint16_t yc, uint16_t r, bool state)
// draw a circle given center and radius
// Bresenhams Algorith from http://www.pracspedia.com/CG/bresenhamcircle.html
{
int x = 0, y = r;
int pk = 3 - (2 * r);
bool b = false;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
PRINT("\n\nCircle center ", xc); PRINT(",", yc); PRINT(" radius ", r);
b &= drawCirclePoints(xc, yc, x, y, state);
while (x < y)
{
// check for decision parameter and correspondingly update pk, x, y
if (pk <= 0)
{
pk = pk + (4 * x) + 6;
b &= drawCirclePoints(xc, yc, ++x, y, state);
}
else
{
pk = pk + (4 * (x - y)) + 10;
b &= drawCirclePoints(xc, yc, ++x, --y, state);
}
}
update(_updateEnabled);
return(b);
}
bool MD_MAXPanel::drawCircleLines(uint16_t xc, uint16_t yc, uint16_t x, uint16_t y, bool state)
// draw symmetrical circle lines for the filled circle
{
bool b = true;
b &= drawHLine(yc - y, xc - x, xc + x, state);
b &= drawHLine(yc + y, xc - x, xc + x, state);
b &= drawHLine(yc - x, xc - y, xc + y, state);
b &= drawHLine(yc + x, xc - y, xc + y, state);
return(b);
}
bool MD_MAXPanel::drawFillCircle(uint16_t xc, uint16_t yc, uint16_t r, bool state)
// Draw a filled circle given center and radius
// Bresenhams Algorith from http://www.pracspedia.com/CG/bresenhamcircle.html
{
int x = 0, y = r;
int pk = 3 - (2 * r);
bool b = false;
bool u = _updateEnabled;
_D->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
update(false);
PRINT("\n\nFilled Circle center ", xc); PRINT(",", yc); PRINT(" radius ", r);
b &= drawCircleLines(xc, yc, x, y, state);
while (x < y)
{
// check for decision parameter and correspondingly update pk, x, y
if (pk <= 0)
{
pk = pk + (4 * x) + 6;
b &= drawCircleLines(xc, yc, ++x, y, state);
}
else
{
pk = pk + (4 * (x - y)) + 10;
b &= drawCircleLines(xc, yc, ++x, --y, state);
}
}
update(u);
return(b);
}
uint16_t MD_MAXPanel::Y2Row(uint16_t x, uint16_t y)
// Convert y coord to linear coord
{
uint16_t Y;
if (_rotatedDisplay)
{
x = getXMax() - x;
Y = (ROW_SIZE - (x % ROW_SIZE) - 1);
}
else
Y = (ROW_SIZE - (y % ROW_SIZE) - 1);
return(Y);
}
uint16_t MD_MAXPanel::X2Col(uint16_t x, uint16_t y)
// Convert x coord to linear coord
{
uint16_t X;
if (_rotatedDisplay)
{
x = getXMax() - x;
X = ((x / ROW_SIZE) * (_xDevices * COL_SIZE) + (_xDevices * COL_SIZE) - 1 - (y % (_xDevices * COL_SIZE)));
}
else
X = ((y / ROW_SIZE) * (_xDevices * COL_SIZE) + (_xDevices * COL_SIZE) - 1 - (x % (_xDevices * COL_SIZE)));
return(X);
}
bool MD_MAXPanel::getPoint(uint16_t x, uint16_t y)
{
if (x > getXMax() || y > getYMax())
return(false);
return(_D->getPoint(Y2Row(x,y), X2Col(x,y)));
}
bool MD_MAXPanel::setPoint(uint16_t x, uint16_t y, bool state)
{
if (x > getXMax() || y > getYMax())
return(false);
//PRINT("[", x); PRINT(",", y); PRINTS("]");
return(_D->setPoint(Y2Row(x,y), X2Col(x,y), state));
}

View File

@@ -0,0 +1,618 @@
#ifndef MD_MAXPANEL_h
#define MD_MAXPANEL_h
#include <Arduino.h>
#include <MD_MAX72xx.h>
/**
* \file
* \brief Main header file for the MD_MAXPanel library
*/
/**
\mainpage Arduino LED Matrix Panel Library
The MD_MAXPanel Library
-----------------------
This library implements functions that allows cascaded MAX72xx LED modules
(64 individual LEDs)to be used for LED matrix panels, allowing the programmer
to use the LED matrix as an pixel addressable display device, as shown in the
photo below.
![MD_MAXPanel Display Panel] (MAXPanel_Display.jpg "MD_MAXPanel Display Panel")
The MAX7219/MAX7221 are compact, serial input/output display drivers that
interface microprocessors to 7-segment numeric LED displays of up to 8 digits,
bar-graph displays, or 64 individual LEDs.
A 4-wire serial interface (SPI) allows the devices to be cascaded, with
communications passed through the first device in the chain to all others.
Individual elements may be addressed and updated without rewriting the entire
display.
In order for this library to work, the MD_MAX72xx library must be installed and
the correct LED module type selected. The individual LED modules must also be
arranged in a zig-zag fashion, as shown in the figure below. The number of modules
per row and the number of rows may vary, but the arrangement of the modules must
follow the example.
![MD_MAXPanel Module Arrangement] (MAXPanel_Diagram.jpg "MD_MAXPanel Module Arrangement")
The wiring for the modules can be simplified as the only signal that needs to be truly
cascaded is the MD_MAX72xx IC Data Out to the next IC Data In. The rest can be wired in
parallel, as shown in the photo.
![MD_MAXPanel Module Wiring] (MAXPanel_Wiring.jpg "MD_MAXPanel Module Wiring")
Topics
------
- \subpage pageSoftware
- \subpage pageRevisionHistory
- \subpage pageCopyright
- \subpage pageDonation
\page pageDonation Support the Library
If you like and use this library please consider making a small donation using [PayPal](https://paypal.me/MajicDesigns/4USD)
\page pageCopyright Copyright
Copyright (C) 2018 Marco Colli. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
\page pageRevisionHistory Revision History
Nov 2018 version 1.2.3
- Corrected default parameters for WEMOS compiler
Oct 2018 version 1.2.2
- Corrected some documentation mistakes
Oct 2018 version 1.2.1
- Updated library.properties and READ.MD files
Sep 2018 version 1.2.0
- Fixed reported WEMOS D1 compiler issues
- Added polygon Fill functions contributed by AndreasPercher
Jul 2018 version 1.1.1
- Finalized preferred orientation for examples
Jul 2018 version 1.1.0
- Renamed textRotation_t to rotation_t
- Allow rotation of display (eg, landscape to portrait)
- Restored missing bricks example
Jun 2018 version 1.0.1
- Extracted common elements of examples in .h file
Jun 2018 version 1.0.0
- First Release
\page pageSoftware Software Library
The Library
-----------
The library implements functions that allow the MAX72xx matrix modules
to be used cascaded and built up into LED matrix panels. This allows the
programmer to control the individual panel LEDs using cartesian coordinates.
The library provides support for standard graphics elements (such as lines,
triangles, rectangles, circles) and text.
The origin for the coordinate system is always in the lowest left hand corner.
- X coordinate increase to the right.
- Y coordinate increase to upwards.
In trigonometric terms, the display is located in the first quadrant.
The library is relies on the related MD_MAX72xx library to provide the
device control elements.
*/
/**
* Core object for the MD_MAXPanel library
*/
class MD_MAXPanel
{
public:
/**
* Rotation enumerated type specification.
*
* Used to define rotation orientation (eg, text or display).
* For text the normal rotation is the standard Latin language left to right
* orientation. Rotation is specified anchored to the first character of the
* string - 0 points >, 90 ^, 180 < and 270 v.
* For the display rotation 0 and 180 and are identical and 90 and 270 are identical.
* The rotation will shift the display between landscape and portrait mode.
*/
enum rotation_t
{
ROT_0, ///< Rotation 0 degrees
ROT_90, ///< Rotation 90 degrees
ROT_180, ///< Rotation 180 degrees
ROT_270, ///< Rotation 270 degrees
};
/**
* Class Constructor - arbitrary digital interface.
*
* Instantiate a new instance of the class. The parameters passed are used to
* connect the software to the hardware. Multiple instances may co-exist
* but they should not share the same hardware CS pin (SPI interface).
*
* \param mod the hardware module being used, one of the MD_MAX72XX::moduleType_t values
* \param dataPin output on the Arduino where data gets shifted out.
* \param clkPin output for the clock signal.
* \param csPin output for selecting the device.
* \param xDevices number of LED matrix modules for the width of the panel.
* \param yDevices number of LED matrix modules for the height of the panel.
*/
MD_MAXPanel(MD_MAX72XX::moduleType_t mod, uint8_t dataPin, uint8_t clkPin, uint8_t csPin, uint8_t xDevices, uint8_t yDevices);
/**
* Class Constructor - SPI hardware interface.
*
* Instantiate a new instance of the class. The parameters passed are used to
* connect the software to the hardware. Multiple instances may co-exist
* but they should not share the same hardware CS pin (SPI interface).
* The dataPin and the clockPin are defined by the Arduino hardware definition
* (SPI MOSI and SCK signals).
*
* \param mod the hardware module being used, one of the MD_MAX72XX::moduleType_t values
* \param csPin output for selecting the device.
* \param xDevices number of LED matrix modules for the width of the panel.
* \param yDevices number of LED matrix modules for the height of the panel.
*/
MD_MAXPanel(MD_MAX72XX::moduleType_t mod, uint8_t csPin, uint8_t xDevices, uint8_t yDevices);
/**
* Class Constructor - Existing MD_MAX72XX object.
*
* Instantiate a new instance of the class. The parameters passed are used to
* connect the software to the hardware. The MD_MAX72X object has been created
* outside this object and it is up to the programmer to ensure that the correct
* parameters are used when this object is created.
*
* \param D pointer to the existing MD_MAX72XX object.
* \param xDevices number of LED matrix modules for the width of the panel.
* \param yDevices number of LED matrix modules for the height of the panel.
*/
MD_MAXPanel(MD_MAX72XX *D, uint8_t xDevices, uint8_t yDevices);
/**
* Initialize the object.
*
* Initialize the object data. This needs to be called during setup() to initialize
* new data for the class that cannot be done during the object creation.
*/
void begin(void);
/**
* Class Destructor.
*
* Released allocated memory and does the necessary to clean up once the object is
* no longer required.
*/
~MD_MAXPanel();
//--------------------------------------------------------------
/** \name Methods for object management.
* @{
*/
/**
* Clear all the display data on all the display devices.
*
* \return No return value.
*/
void clear(void) { _D->clear(0, _xDevices*_yDevices); };
/**
* Clear the specified display area.
*
* Clear the rectangular area specified by the coordinates by setting the pixels
* to the state specified (default is off).
*
* \param x1 the upper left x coordinate of the window
* \param y1 the upper left y coordinate of the window
* \param x2 the lower right x coordinate of the window
* \param y2 the upper lower right y coordinate of the window
* \param state true - switch pixels on; false - switch pixels off. If omitted, default to false.
* \return No return value.
*/
void clear(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state = false) { for (uint8_t i=x1; i<=x2; i++) drawVLine(i, y1, y2, state); };
/**
* Get a pointer to the instantiated graphics object.
*
* Provides a pointer to the MD_MAX72XX object to allow access to
* the display graphics functions.
*
* \return Pointer to the MD_MAX72xx object used by the library.
*/
MD_MAX72XX *getGraphicObject(void) { return(_D); }
/**
* Gets the maximum X coordinate.
*
* Depends on the rotation status of the display.
*
* \return uint8_t the maximum X coordinate.
*/
uint16_t getXMax(void);
/**
* Gets the maximum Y coordinate.
*
* Depends on the rotation status of the display.
*
* \return uint16_t representing the number of columns.
*/
uint16_t getYMax(void);
/**
* Get the rotation status of the display
*
* \return rotation_t value for the current rotation (ROT_0 or ROT_90)
*/
rotation_t getRotation(void) { return(_rotatedDisplay ? ROT_90 : ROT_0); }
/**
* Set rotation status of the display
*
* Set the display to be rotated or not. ROT_90 and ROT_270 rotate the display
* by 90 degrees in the same direction. ROT_O and ROT_180 are an unrotated display.
* The default is ROT_O (unrotated).
*
* \param r rotation_t value for the current rotation (ROT_0 or ROT_90);
* \return No return value
*/
void setRotation(rotation_t r) { _rotatedDisplay = (r == ROT_90) || (r == ROT_270); }
/**
* Turn auto display updates on or off.
*
* Turn auto updates on and off, as required. When auto updates are turned OFF the
* display will not update after each operation. Display updates can be forced at any
* time using using a call to update() with no parameters.
*
* \param state true to enable update, false to suspend updates.
* \return No return value.
*/
void update(bool state) { _updateEnabled = state; _D->control(MD_MAX72XX::UPDATE, state ? MD_MAX72XX::ON : MD_MAX72XX::OFF); };
/**
* Force a display update.
*
* Force a display update of any changes since the last update. This overrides the
* current setting for display updates.
*
* \return No return value.
*/
void update() { _D->update(); };
/**
* Set the display intensity.
*
* Set the intensity (brightness) of the display.
*
* \param intensity the intensity to set the display (0-15).
* \return No return value.
*/
void setIntensity(uint8_t intensity) { _D->control(MD_MAX72XX::INTENSITY, intensity); }
/** @} */
//--------------------------------------------------------------
/** \name Methods for drawing graphics.
* @{
*/
/**
* Draw a horizontal line between two points on the display
*
* Draw a horizontal line between the specified points. The LED will be turned on or
* off depending on the value supplied. The column number will be dereferenced
* into the device and column within the device, allowing the LEDs to be treated
* as a continuous pixel field.
*
* \param y y coordinate for the line [0..getYMax()].
* \param x1 starting x coordinate for the point [0..getXMax()].
* \param x2 ending x coordinate for the point [0..getXMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawHLine(uint16_t y, uint16_t x1, uint16_t x2, bool state = true);
/**
* Draw an arbitrary line between two points on the display
*
* Draw a line between the specified points using Bresenham's algorithm.
* The LED will be turned on or off depending on the value supplied. The
* column number will be dereferenced into the device and column within
* the device, allowing the LEDs to be treated as a continuous pixel field.
*
* \param x1 starting x coordinate for the point [0..getXMax()].
* \param y1 starting y coordinate for the point [0..getYMax()].
* \param x2 ending x coordinate for the point [0..getXMax()].
* \param y2 ending y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state = true);
/**
* Draw a vertical line between two points on the display
*
* Draw a horizontal line between the specified points. The LED will be turned on or
* off depending on the value supplied. The column number will be dereferenced
* into the device and column within the device, allowing the LEDs to be treated
* as a continuous pixel field.
*
* \param x x coordinate for the line [0..getXMax()].
* \param y1 starting y coordinate for the point [0..getYMax()].
* \param y2 ending y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawVLine(uint16_t x, uint16_t y1, uint16_t y2, bool state = true);
/**
* Draw a rectangle given two diagonal vertices
*
* Draw a rectangle given the points across the diagonal. The LED will be turned on or
* off depending on the value supplied. The coordinates will be dereferenced
* into the device and column within the device, allowing the LEDs to be treated
* as a continuous pixel field.
*
* \param x1 starting x coordinate for the point [0..getXMax()].
* \param y1 starting y coordinate for the point [0..getYMax()].
* \param x2 ending x coordinate for the point [0..getXMax()].
* \param y2 ending y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state = true);
/**
* Draw a filled rectangle given two diagonal vertices
*
* Draw a filled rectangle given the points across the diagonal. The LEDs inside
* and on the border will be turned on or off depending on the value supplied.
* The coordinates will be dereferenced into the device and column within the
* device, allowing the LEDs to be treated as a continuous pixel field.
*
* \param x1 starting x coordinate for the point [0..getXMax()].
* \param y1 starting y coordinate for the point [0..getYMax()].
* \param x2 ending x coordinate for the point [0..getXMax()].
* \param y2 ending y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawFillRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool state = true);
/**
* Draw a triangle given 3 vertices
*
* Draw a triangle given the all the corner points. The LED will be turned on or
* off depending on the value supplied. The coordinates will be dereferenced
* into the device and column within the device, allowing the LEDs to be treated
* as a continuous pixel field.
*
* \param x1 first x coordinate for the point [0..getXMax()].
* \param y1 first y coordinate for the point [0..getYMax()].
* \param x2 second x coordinate for the point [0..getXMax()].
* \param y2 second y coordinate for the point [0..getYMax()].
* \param x3 third x coordinate for the point [0..getXMax()].
* \param y3 third y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, bool state = true);
/**
* Draw a filled triangle given 3 vertices
*
* Draw a filled triangle given the all the corner points. The LED for
* the border and fill will be turned on or off depending on the value
* supplied. The coordinates will be dereferenced into the device and
* column within the device, allowing the LEDs to be treated as a
* continuous pixel field.
*
* \param x1 first x coordinate for the point [0..getXMax()].
* \param y1 first y coordinate for the point [0..getYMax()].
* \param x2 second x coordinate for the point [0..getXMax()].
* \param y2 second y coordinate for the point [0..getYMax()].
* \param x3 third x coordinate for the point [0..getXMax()].
* \param y3 third y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawFillTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, bool state = true);
/**
* Draw a quadrilateral given 4 vertices
*
* Draw a quadrilateral given the all the corner points. The LED will be turned on or
* off depending on the value supplied. The coordinates will be dereferenced
* into the device and column within the device, allowing the LEDs to be treated
* as a continuous pixel field.
*
* \param x1 first x coordinate for the point [0..getXMax()].
* \param y1 first y coordinate for the point [0..getYMax()].
* \param x2 second x coordinate for the point [0..getXMax()].
* \param y2 second y coordinate for the point [0..getYMax()].
* \param x3 third x coordinate for the point [0..getXMax()].
* \param y3 third y coordinate for the point [0..getYMax()].
* \param x4 fourth x coordinate for the point [0..getXMax()].
* \param y4 fourth y coordinate for the point [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawQuadrilateral(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, uint16_t x4, uint16_t y4, bool state = true);
/**
* Draw a circle given center and radius
*
* Draw a circle given center and radius. The LEDs will be turned
* on or off depending on the value supplied. The coordinates will
* be dereferenced into the device and column within the device,
* allowing the LEDs to be treated as a continuous pixel field.
*
* \param xc x coordinate for the center point [0..getXMax()].
* \param yc y coordinate for the center point [0..getYMax()].
* \param r radius of the circle.
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawCircle(uint16_t xc, uint16_t yc, uint16_t r, bool state = true);
/**
* Draw a filled circle given center and radius
*
* Draw a circle given center and radius. The LEDs for the border
* and fill will be turned on or off depending on the value supplied.
* The coordinates will be dereferenced into the device and column
* within the device, allowing the LEDs to be treated as a continuous
* pixel field.
*
* \param xc x coordinate for the center point [0..getXMax()].
* \param yc y coordinate for the center point [0..getYMax()].
* \param r radius of the circle.
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if any point is drawn outside the display, true otherwise.
*/
bool drawFillCircle(uint16_t xc, uint16_t yc, uint16_t r, bool state = true);
/**
* Get the status of a single LED, addressed as a pixel.
*
* The method will get the status of a specific LED element based on its
* coordinate position. The column number is dereferenced into the device
* and column within the device, allowing the LEDs to be treated as a
* planar pixel field.
*
* \param x x coordinate [0..getXMax()].
* \param y y coordinate [0..getYMax()].
* \return true if LED is on, false if off or parameter errors.
*/
bool getPoint(uint16_t x, uint16_t y);
/**
* Set the status of a single LED, addressed as a pixel.
*
* The method will set the value of a specific LED element based on its
* coordinate position. The LED will be turned on or off depending on the
* value supplied. The column number is dereferenced into the device and
* column within the device, allowing the LEDs to be treated as a
* planar pixel field.
*
* \param x x coordinate [0..getXMax()].
* \param y y coordinate [0..getYMax()].
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return false if parameter errors, true otherwise.
*/
bool setPoint(uint16_t x, uint16_t y, bool state = true);
/** @} */
//--------------------------------------------------------------
/** \name Methods for Fonts and text.
* @{
*/
/**
* Set the display font.
*
* Set the display font to a user defined font table. This can be created using the
* MD_MAX72xx font builder (refer to documentation for the tool and the MD_MAX72xx library).
* Passing nullptr resets to the library default font.
*
* \param fontDef Pointer to the font definition to be used.
* \return No return value.
*/
void setFont(MD_MAX72XX::fontType_t *fontDef) { _D->setFont(fontDef); }
/**
* Set the spacing between characters.
*
* Set number of pixel columns between each character in a displayed text.
*
* \param spacing the spacing between characters.
* \return No return value.
*/
void setCharSpacing(uint8_t spacing) { _charSpacing = spacing; }
/**
* Get the spacing between characters.
*
* Get number of pixel columns between each character in a displayed text.
*
* \return the spacing between characters.
*/
uint8_t getCharSpacing(void) { return(_charSpacing); }
/**
* Get the length of a text string in pixels.
*
* Get the length of a string in pixels. The text is a nul terminated characters array.
* The returned length will include all inter-character Set number of pixel columns between each character in a displayed text.
*
* \param psz the text string as a nul terminated character array.
* \return the length in pixels.
*/
uint16_t getTextWidth(const char *psz);
/**
* Get the height of the current font in pixels.
*
* Get the specified of height of the current font in pixels. All the characters of the
* font will fit with this height.
*
* \return the height in pixels.
*/
uint16_t getFontHeight(void) { return(_D->getFontHeight()); }
/**
* Draw text on the display.
*
* Get the specified of height of the current font in pixels. All the characters of the
* font will fit with this height.
*
* \param x the x coordinate for the top left corner of the first character.
* \param y the Y coordinate for the top left corner of the first character.
* \param psz the text to be displayed as a nul terminated character array.
* \param rot the required rotation orientation for the text as described in textRotation_t. Default is ROT_0.
* \param state true - switch on; false - switch off. If omitted, default to true.
* \return the length of the text in pixels.
*/
uint16_t drawText(uint16_t x, uint16_t y, const char *psz, rotation_t rot = ROT_0, bool state = true);
/** @} */
private:
// Device buffer data
uint8_t _xDevices; // number of devices in the width of the panel
uint8_t _yDevices; // number of devices in the height of the panel
MD_MAX72XX *_D; // hardware driver
bool _killOnDestruct; // true if we have allocated the MD_MAX72XX object
bool _updateEnabled; // true if display updates are suspended
uint8_t _charSpacing; // number of pixel columns between characters
bool _rotatedDisplay; // true if the display is rotated
bool drawCirclePoints(uint16_t xc, uint16_t yc, uint16_t x, uint16_t y, bool state);
bool drawCircleLines(uint16_t xc, uint16_t yc, uint16_t x, uint16_t y, bool state);
uint16_t Y2Row(uint16_t x, uint16_t y); // Convert y coord to linear coord
uint16_t X2Col(uint16_t x, uint16_t y); // Convert x coord to linear coord
};
#endif

View File

@@ -0,0 +1,87 @@
/*
MD_MAXPanel - Library for MAX7219/7221 LED Panel
See header file for comments
This file contains font and text related methods.
*/
#include <Arduino.h>
#include "MD_MAXPanel.h"
#include "MD_MAXPanel_lib.h"
/**
* \file
* \brief Implements font and text methods
*/
uint16_t MD_MAXPanel::getTextWidth(const char *psz)
{
uint16_t sum = 0;
uint8_t bufSize = _D->getMaxFontWidth();
uint8_t buf[bufSize];
while (*psz != '\0')
{
sum += _D->getChar(*psz++, bufSize, buf);
if (*psz) sum += _charSpacing; // next character is not nul, so add inter-character spacing
}
return(sum);
}
uint16_t MD_MAXPanel::drawText(uint16_t x, uint16_t y, const char *psz, rotation_t rot, bool state)
{
uint8_t height = _D->getFontHeight();
uint8_t bufSize = _D->getMaxFontWidth() + _charSpacing;
uint8_t buf[bufSize];
uint8_t size;
uint16_t sum = 0;
int16_t nextPos;
PRINT("\ndrawText: ", psz);
PRINT(" height ", height);
while (*psz != '\0')
{
PRINT("\nChar ", *psz);
memset(buf, '\0', bufSize*sizeof(uint8_t));
size = _D->getChar(*psz, bufSize, buf);
psz++;
if (*psz != '\0') size += _charSpacing; // add in the blank columns
sum += size;
// now display the char depending on what the orientation is
switch (rot)
{
case ROT_0:
for (uint16_t i = 0; i < size; i++)
for (uint16_t j = 0; j < height; j++)
setPoint(x + i, y - j, (buf[i] & (1 << j) ? state : !state));
x += size;
break;
case ROT_90:
for (uint16_t j = 0; j < size; j++)
for (uint16_t i = 0; i < height; i++)
setPoint(x + i, y + j, (buf[j] & (1 << i) ? state : !state));
y += size;
break;
case ROT_180:
for (uint16_t i = 0; i < size; i++)
for (uint16_t j = 0; j < height; j++)
setPoint(x - i, y + j, (buf[i] & (1 << j) ? state : !state));
x -= size;
break;
case ROT_270:
for (uint16_t j = 0; j < size; j++)
for (uint16_t i = 0; i < height; i++)
setPoint(x - i, y - j, (buf[j] & (1 << i) ? state : !state));
y -= size;
break;
}
}
return(sum);
}

View File

@@ -0,0 +1,49 @@
/*
MD_MAXPanel - Library for panels of MAX72xx LED matrix controllers
See header file for comments
This file contains library related definitions and is not visible
to user code.
Copyright (C) 2018 Marco Colli. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
/**
* \file
* \brief Includes library only definitions
*/
#define MP_DEBUG 0 ///< Enable or disable (default) debugging output from the MD_MAX72xx library
#if MP_DEBUG
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); } ///< Print a string followed by a value (decimal)
#define PRINTX(s, v) { Serial.print(F(s)); Serial.print(v, HEX); } ///< Print a string followed by a value (hex)
#define PRINTB(s, v) { Serial.print(F(s)); Serial.print(v, BIN); } ///< Print a string followed by a value (binary)
#define PRINTS(s) { Serial.print(F(s)); } ///< Print a string
#else
#define PRINT(s, v) ///< Print a string followed by a value (decimal)
#define PRINTX(s, v) ///< Print a string followed by a value (hex)
#define PRINTB(s, v) ///< Print a string followed by a value (binary)
#define PRINTS(s) ///< Print a string
#endif
#define X2COL(x, y) (((y / ROW_SIZE) * (getXMax() + 1)) + (getXMax() - (x % (getXMax() + 1)))) ///< Convert x coord to linear coord
#define Y2ROW(x, y) (ROW_SIZE - (y % ROW_SIZE) - 1) ///< Convert y coord to linear coord
#define CHAR_SPACING_DEFAULT 1 ///< Default number of pixels between characters