Compare commits
16 Commits
wip/persis
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
4a41ace4d0 | |
|
|
f711ca0a55 | |
|
|
ad7ffaac31 | |
|
|
99a2fa31c4 | |
|
|
1c3980d700 | |
|
|
b2a118a132 | |
|
|
ac4ce81638 | |
|
|
f193e4e691 | |
|
|
ad29c3d59c | |
|
|
a2013adea9 | |
|
|
ac864a5fa5 | |
|
|
fc4da3bcc5 | |
|
|
8b894b4c8c | |
|
|
082f0bba38 | |
|
|
54f065be1e | |
|
|
79c8c464b6 |
|
|
@ -4,3 +4,5 @@
|
||||||
build-and-send.fish
|
build-and-send.fish
|
||||||
werewolves-saves/
|
werewolves-saves/
|
||||||
werewolves/img/icons.svg
|
werewolves/img/icons.svg
|
||||||
|
license_headers.fish
|
||||||
|
util/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,660 @@
|
||||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||||
|
<https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
## Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains
|
||||||
|
free software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, 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
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing
|
||||||
|
under this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
## TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
### 0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds
|
||||||
|
of works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of
|
||||||
|
an exact copy. The resulting work is called a "modified version" of
|
||||||
|
the earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user
|
||||||
|
through a computer network, with no transfer of a copy, is not
|
||||||
|
conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" to
|
||||||
|
the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
### 1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. "Object code" means any non-source form of
|
||||||
|
a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can
|
||||||
|
regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same
|
||||||
|
work.
|
||||||
|
|
||||||
|
### 2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not convey,
|
||||||
|
without conditions so long as your license otherwise remains in force.
|
||||||
|
You may convey covered works to others for the sole purpose of having
|
||||||
|
them make modifications exclusively for you, or provide you with
|
||||||
|
facilities for running those works, provided that you comply with the
|
||||||
|
terms of this License in conveying all material for which you do not
|
||||||
|
control copyright. Those thus making or running the covered works for
|
||||||
|
you must do so exclusively on your behalf, under your direction and
|
||||||
|
control, on terms that prohibit them from making any copies of your
|
||||||
|
copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the
|
||||||
|
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||||
|
it unnecessary.
|
||||||
|
|
||||||
|
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such
|
||||||
|
circumvention is effected by exercising rights under this License with
|
||||||
|
respect to the covered work, and you disclaim any intention to limit
|
||||||
|
operation or modification of the work as a means of enforcing, against
|
||||||
|
the work's users, your or third parties' legal rights to forbid
|
||||||
|
circumvention of technological measures.
|
||||||
|
|
||||||
|
### 4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
### 5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
- a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
- b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under
|
||||||
|
section 7. This requirement modifies the requirement in section 4
|
||||||
|
to "keep intact all notices".
|
||||||
|
- c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
- d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
### 6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms of
|
||||||
|
sections 4 and 5, provided that you also convey the machine-readable
|
||||||
|
Corresponding Source under the terms of this License, in one of these
|
||||||
|
ways:
|
||||||
|
|
||||||
|
- a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
- b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the Corresponding
|
||||||
|
Source from a network server at no charge.
|
||||||
|
- c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
- d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
- e) Convey the object code using peer-to-peer transmission,
|
||||||
|
provided you inform other peers where the object code and
|
||||||
|
Corresponding Source of the work are being offered to the general
|
||||||
|
public at no charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal,
|
||||||
|
family, or household purposes, or (2) anything designed or sold for
|
||||||
|
incorporation into a dwelling. In determining whether a product is a
|
||||||
|
consumer product, doubtful cases shall be resolved in favor of
|
||||||
|
coverage. For a particular product received by a particular user,
|
||||||
|
"normally used" refers to a typical or common use of that class of
|
||||||
|
product, regardless of the status of the particular user or of the way
|
||||||
|
in which the particular user actually uses, or expects or is expected
|
||||||
|
to use, the product. A product is a consumer product regardless of
|
||||||
|
whether the product has substantial commercial, industrial or
|
||||||
|
non-consumer uses, unless such uses represent the only significant
|
||||||
|
mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to
|
||||||
|
install and execute modified versions of a covered work in that User
|
||||||
|
Product from a modified version of its Corresponding Source. The
|
||||||
|
information must suffice to ensure that the continued functioning of
|
||||||
|
the modified object code is in no case prevented or interfered with
|
||||||
|
solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or
|
||||||
|
updates for a work that has been modified or installed by the
|
||||||
|
recipient, or for the User Product in which it has been modified or
|
||||||
|
installed. Access to a network may be denied when the modification
|
||||||
|
itself materially and adversely affects the operation of the network
|
||||||
|
or violates the rules and protocols for communication across the
|
||||||
|
network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
### 7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders
|
||||||
|
of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
- a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
- b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
- c) Prohibiting misrepresentation of the origin of that material,
|
||||||
|
or requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
- d) Limiting the use for publicity purposes of names of licensors
|
||||||
|
or authors of the material; or
|
||||||
|
- e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
- f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions
|
||||||
|
of it) with contractual assumptions of liability to the recipient,
|
||||||
|
for any liability that these contractual assumptions directly
|
||||||
|
impose on those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions; the
|
||||||
|
above requirements apply either way.
|
||||||
|
|
||||||
|
### 8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your license
|
||||||
|
from a particular copyright holder is reinstated (a) provisionally,
|
||||||
|
unless and until the copyright holder explicitly and finally
|
||||||
|
terminates your license, and (b) permanently, if the copyright holder
|
||||||
|
fails to notify you of the violation by some reasonable means prior to
|
||||||
|
60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
### 9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or run
|
||||||
|
a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
### 10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
### 11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims owned
|
||||||
|
or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within the
|
||||||
|
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||||
|
the non-exercise of one or more of the rights that are specifically
|
||||||
|
granted under this License. You may not convey a covered work if you
|
||||||
|
are a party to an arrangement with a third party that is in the
|
||||||
|
business of distributing software, under which you make payment to the
|
||||||
|
third party based on the extent of your activity of conveying the
|
||||||
|
work, and under which the third party grants, to any of the parties
|
||||||
|
who would receive the covered work from you, a discriminatory patent
|
||||||
|
license (a) in connection with copies of the covered work conveyed by
|
||||||
|
you (or copies made from those copies), or (b) primarily for and in
|
||||||
|
connection with specific products or compilations that contain the
|
||||||
|
covered work, unless you entered into that arrangement, or that patent
|
||||||
|
license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
### 12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under
|
||||||
|
this License and any other pertinent obligations, then as a
|
||||||
|
consequence you may not convey it at all. For example, if you agree to
|
||||||
|
terms that obligate you to collect a royalty for further conveying
|
||||||
|
from those to whom you convey the Program, the only way you could
|
||||||
|
satisfy both those terms and this License would be to refrain entirely
|
||||||
|
from conveying the Program.
|
||||||
|
|
||||||
|
### 13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your
|
||||||
|
version supports such interaction) an opportunity to receive the
|
||||||
|
Corresponding Source of your version by providing access to the
|
||||||
|
Corresponding Source from a network server at no charge, through some
|
||||||
|
standard or customary means of facilitating copying of software. This
|
||||||
|
Corresponding Source shall include the Corresponding Source for any
|
||||||
|
work covered by version 3 of the GNU General Public License that is
|
||||||
|
incorporated pursuant to the following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
### 14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Affero 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 Program
|
||||||
|
specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever
|
||||||
|
published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions
|
||||||
|
of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
### 15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||||
|
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
||||||
|
CORRECTION.
|
||||||
|
|
||||||
|
### 16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
||||||
|
CONVEYS THE PROGRAM 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 PROGRAM (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 PROGRAM
|
||||||
|
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||||
|
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
### 17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
## How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these
|
||||||
|
terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to
|
||||||
|
attach them to the start of each source file to most effectively state
|
||||||
|
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 program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper
|
||||||
|
mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for
|
||||||
|
the specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. For more information on this, and how to apply and follow
|
||||||
|
the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
drop table if exists users cascade;
|
|
||||||
create table users (
|
|
||||||
id uuid not null default gen_random_uuid() primary key,
|
|
||||||
name text,
|
|
||||||
username text not null,
|
|
||||||
password_hash text not null,
|
|
||||||
|
|
||||||
created_at timestamp with time zone not null,
|
|
||||||
updated_at timestamp with time zone not null,
|
|
||||||
|
|
||||||
check (created_at <= updated_at)
|
|
||||||
);
|
|
||||||
drop index if exists users_username_idx;
|
|
||||||
create index users_username_idx on users (username);
|
|
||||||
drop index if exists users_username_unique;
|
|
||||||
create unique index users_username_unique on users (lower(username));
|
|
||||||
|
|
||||||
drop table if exists login_tokens cascade;
|
|
||||||
create table login_tokens (
|
|
||||||
token text not null primary key,
|
|
||||||
user_id uuid not null references users(id),
|
|
||||||
created_at timestamp with time zone not null,
|
|
||||||
expires_at timestamp with time zone not null,
|
|
||||||
|
|
||||||
check (created_at < expires_at)
|
|
||||||
);
|
|
||||||
|
|
||||||
drop type if exists game_outcome cascade;
|
|
||||||
create type game_outcome as enum (
|
|
||||||
'village_victory',
|
|
||||||
'wolves_victory'
|
|
||||||
);
|
|
||||||
|
|
||||||
drop table if exists games cascade;
|
|
||||||
create table games (
|
|
||||||
id uuid not null primary key,
|
|
||||||
outcome game_outcome,
|
|
||||||
state json not null,
|
|
||||||
story json not null,
|
|
||||||
|
|
||||||
started_at timestamp with time zone not null,
|
|
||||||
updated_at timestamp with time zone not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
drop table if exists players;
|
|
||||||
create table players (
|
|
||||||
id uuid not null primary key,
|
|
||||||
user_id uuid references users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
drop table if exists game_players;
|
|
||||||
create table game_players (
|
|
||||||
game_id uuid not null references games(id),
|
|
||||||
player_id uuid not null references players(id),
|
|
||||||
|
|
||||||
primary key (game_id, player_id)
|
|
||||||
);
|
|
||||||
|
|
@ -10,4 +10,5 @@ proc-macro = true
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||||
convert_case = { version = "0.8" }
|
convert_case = { version = "0.9" }
|
||||||
|
chrono = { version = "0.4" }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::hash::Hash;
|
use core::hash::Hash;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,22 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::error::Error;
|
use core::error::Error;
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use convert_case::Casing;
|
use convert_case::Casing;
|
||||||
|
|
@ -560,3 +575,73 @@ pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
||||||
quote! {#ref_and_mut}.into()
|
quote! {#ref_and_mut}.into()
|
||||||
}
|
}
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn build_dirty(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
if !input.is_empty() {
|
||||||
|
panic!("build_dirty doesn't take arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
let git_state = Command::new("git")
|
||||||
|
.arg("diff")
|
||||||
|
.arg("--stat")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
if !git_state.status.success() {
|
||||||
|
panic!("git diff --stat failed");
|
||||||
|
}
|
||||||
|
let dirty = !git_state.stdout.is_empty();
|
||||||
|
|
||||||
|
quote! {#dirty}.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn build_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
if !input.is_empty() {
|
||||||
|
panic!("build_id doesn't take arguments");
|
||||||
|
}
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("rev-parse")
|
||||||
|
.arg("--short")
|
||||||
|
.arg("HEAD")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!("git rev-parse --short HEAD failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = String::from_utf8(output.stdout).unwrap();
|
||||||
|
let git_ref = output.trim();
|
||||||
|
|
||||||
|
quote! {#git_ref}.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn build_id_long(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
if !input.is_empty() {
|
||||||
|
panic!("build_id doesn't take arguments");
|
||||||
|
}
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("rev-parse")
|
||||||
|
.arg("HEAD")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
if !output.status.success() {
|
||||||
|
panic!("git rev-parse --short HEAD failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = String::from_utf8(output.stdout).unwrap();
|
||||||
|
let git_ref = output.trim();
|
||||||
|
|
||||||
|
quote! {#git_ref}.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn build_time(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
if !input.is_empty() {
|
||||||
|
panic!("build_time doesn't take arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = chrono::Utc::now().format("%d %b %Y %T UTC").to_string();
|
||||||
|
|
||||||
|
quote! {#time}.into()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{parse::Parse, spanned::Spanned};
|
use syn::parse::Parse;
|
||||||
|
#[allow(unused)]
|
||||||
pub struct RefAndMut {
|
pub struct RefAndMut {
|
||||||
name: syn::Ident,
|
name: syn::Ident,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{ToTokens, quote, quote_spanned};
|
use quote::{ToTokens, quote, quote_spanned};
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,10 @@ log = { version = "0.4" }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||||
rand = { version = "0.9" }
|
rand = { version = "0.9", features = ["std_rng"] }
|
||||||
werewolves-macros = { path = "../werewolves-macros" }
|
werewolves-macros = { path = "../werewolves-macros" }
|
||||||
axum = { version = "*", optional = true }
|
|
||||||
argon2 = { version = "*", optional = true }
|
|
||||||
sqlx = { version = "*", optional = true }
|
|
||||||
ciborium = { version = "*", optional = true }
|
|
||||||
bytes = { version = "1.10.1", features = ["serde"], optional = true }
|
|
||||||
axum-extra = { version = "*", optional = true }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1" }
|
pretty_assertions = { version = "1" }
|
||||||
pretty_env_logger = { version = "0.5" }
|
pretty_env_logger = { version = "0.5" }
|
||||||
colored = { version = "3.0" }
|
colored = { version = "3.0" }
|
||||||
|
|
||||||
[features]
|
|
||||||
server = [
|
|
||||||
"dep:axum",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:argon2",
|
|
||||||
"dep:ciborium",
|
|
||||||
"dep:bytes",
|
|
||||||
"dep:axum-extra",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bag::DrunkBag,
|
||||||
|
game::{GameTime, Village},
|
||||||
|
role::{Alignment, Killer, Powerful},
|
||||||
|
team::Team,
|
||||||
|
};
|
||||||
|
const BLOODLET_DURATION_DAYS: u8 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs, Titles)]
|
||||||
|
pub enum Aura {
|
||||||
|
#[checks("assignable")]
|
||||||
|
Traitor,
|
||||||
|
#[checks("assignable")]
|
||||||
|
#[checks("cleansible")]
|
||||||
|
Drunk(DrunkBag),
|
||||||
|
#[checks("assignable")]
|
||||||
|
Insane,
|
||||||
|
#[checks("cleansible")]
|
||||||
|
Bloodlet { night: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Aura {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Aura::Traitor => "Traitor",
|
||||||
|
Aura::Drunk(_) => "Drunk",
|
||||||
|
Aura::Insane => "Insane",
|
||||||
|
Aura::Bloodlet { .. } => "Bloodlet",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aura {
|
||||||
|
pub const fn expired(&self, village: &Village) -> bool {
|
||||||
|
match self {
|
||||||
|
Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false,
|
||||||
|
Aura::Bloodlet {
|
||||||
|
night: applied_night,
|
||||||
|
} => match village.time() {
|
||||||
|
GameTime::Day { .. } => false,
|
||||||
|
GameTime::Night {
|
||||||
|
number: current_night,
|
||||||
|
} => current_night >= applied_night.saturating_add(BLOODLET_DURATION_DAYS),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn refreshes(&self, other: &Aura) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, other),
|
||||||
|
(Aura::Bloodlet { .. }, Aura::Bloodlet { .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh(&mut self, other: Aura) {
|
||||||
|
if let (Aura::Bloodlet { night }, Aura::Bloodlet { night: new_night }) = (self, other) {
|
||||||
|
*night = new_night
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Auras(Vec<Aura>);
|
||||||
|
|
||||||
|
impl Auras {
|
||||||
|
pub const fn new(auras: Vec<Aura>) -> Self {
|
||||||
|
Self(auras)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> &[Aura] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_mut(&mut self) -> &mut [Aura] {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_aura(&mut self, aura: AuraTitle) {
|
||||||
|
self.0.retain(|a| a.title() != aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, aura: AuraTitle) -> Option<&mut Aura> {
|
||||||
|
self.0.iter_mut().find(|a| a.title() == aura)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// purges expired [Aura]s and returns the ones that were removed
|
||||||
|
pub fn purge_expired(&mut self, village: &Village) -> Box<[Aura]> {
|
||||||
|
let mut auras = Vec::with_capacity(self.0.len());
|
||||||
|
core::mem::swap(&mut self.0, &mut auras);
|
||||||
|
let (expired, retained): (Vec<_>, Vec<_>) =
|
||||||
|
auras.into_iter().partition(|aura| aura.expired(village));
|
||||||
|
|
||||||
|
self.0 = retained;
|
||||||
|
expired.into_boxed_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, aura: Aura) {
|
||||||
|
if let Some(existing) = self.0.iter_mut().find(|aura| aura.refreshes(aura)) {
|
||||||
|
existing.refresh(aura);
|
||||||
|
} else {
|
||||||
|
self.0.push(aura);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanse(&mut self) {
|
||||||
|
self.0.retain(|aura| !aura.cleansible());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override the player's [Team]
|
||||||
|
pub fn overrides_team(&self) -> Option<Team> {
|
||||||
|
if self.0.iter().any(|a| matches!(a, Aura::Traitor)) {
|
||||||
|
return Some(Team::AnyEvil);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override the player's [Alignment]
|
||||||
|
pub fn overrides_alignment(&self) -> Option<Alignment> {
|
||||||
|
for aura in self.0.iter() {
|
||||||
|
match aura {
|
||||||
|
Aura::Traitor => return Some(Alignment::Traitor),
|
||||||
|
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
||||||
|
Aura::Drunk(_) | Aura::Insane => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override whether the player is a [Killer]
|
||||||
|
pub fn overrides_killer(&self) -> Option<Killer> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, Aura::Bloodlet { .. }))
|
||||||
|
.then_some(Killer::Killer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override whether the player is [Powerful]
|
||||||
|
pub fn overrides_powerful(&self) -> Option<Powerful> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, Aura::Bloodlet { .. }))
|
||||||
|
.then_some(Powerful::Powerful)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuraTitle {
|
||||||
|
pub fn into_aura(self) -> Aura {
|
||||||
|
match self {
|
||||||
|
AuraTitle::Traitor => Aura::Traitor,
|
||||||
|
AuraTitle::Drunk => Aura::Drunk(DrunkBag::default()),
|
||||||
|
AuraTitle::Insane => Aura::Insane,
|
||||||
|
AuraTitle::Bloodlet => Aura::Bloodlet { night: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use rand::{SeedableRng, rngs::SmallRng, seq::SliceRandom};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Bag<T>(Vec<T>);
|
||||||
|
|
||||||
|
impl<T> Bag<T> {
|
||||||
|
pub fn new(items: impl IntoIterator<Item = T>) -> Self {
|
||||||
|
Self(items.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull(&mut self) -> Option<T> {
|
||||||
|
self.0.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(&self) -> Option<&T> {
|
||||||
|
self.0.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum DrunkRoll {
|
||||||
|
Drunk,
|
||||||
|
#[default]
|
||||||
|
Sober,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct DrunkBag {
|
||||||
|
#[serde(skip)]
|
||||||
|
rng: SmallRng,
|
||||||
|
seed: u64,
|
||||||
|
bag_number: usize,
|
||||||
|
bag: Bag<DrunkRoll>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for DrunkBag {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DrunkBagNoRng {
|
||||||
|
seed: u64,
|
||||||
|
bag_number: usize,
|
||||||
|
bag: Bag<DrunkRoll>,
|
||||||
|
}
|
||||||
|
let DrunkBagNoRng {
|
||||||
|
seed,
|
||||||
|
bag_number,
|
||||||
|
bag,
|
||||||
|
} = DrunkBagNoRng::deserialize(deserializer)?;
|
||||||
|
let mut rng = SmallRng::seed_from_u64(seed);
|
||||||
|
// Shuffle the default bag bag_number of times to get the smallrng to the same state
|
||||||
|
for _ in 0..bag_number {
|
||||||
|
Self::DEFAULT_BAG
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
|
.shuffle(&mut rng);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
rng,
|
||||||
|
seed,
|
||||||
|
bag_number,
|
||||||
|
bag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DrunkBag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrunkBag {
|
||||||
|
const DEFAULT_BAG: &[DrunkRoll] = &[
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn all_drunk() -> Self {
|
||||||
|
Self {
|
||||||
|
rng: SmallRng::seed_from_u64(0),
|
||||||
|
seed: 0,
|
||||||
|
bag_number: 1,
|
||||||
|
bag: Bag::new([
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let seed = rand::random();
|
||||||
|
let mut rng = SmallRng::seed_from_u64(seed);
|
||||||
|
let mut starting_bag = Self::DEFAULT_BAG.iter().copied().collect::<Box<[_]>>();
|
||||||
|
starting_bag.shuffle(&mut rng);
|
||||||
|
let bag = Bag::new(starting_bag);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rng,
|
||||||
|
seed,
|
||||||
|
bag,
|
||||||
|
bag_number: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(&self) -> DrunkRoll {
|
||||||
|
self.bag.peek().copied().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_bag(&mut self) {
|
||||||
|
let mut starting_bag = Self::DEFAULT_BAG.iter().copied().collect::<Box<[_]>>();
|
||||||
|
starting_bag.shuffle(&mut self.rng);
|
||||||
|
self.bag = Bag::new(starting_bag);
|
||||||
|
self.bag_number += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull(&mut self) -> DrunkRoll {
|
||||||
|
if self.bag.len() < 2 {
|
||||||
|
*self = Self::new();
|
||||||
|
} else if self.bag.len() == 2 {
|
||||||
|
let pulled = self.bag.pull().unwrap_or_default();
|
||||||
|
self.next_bag();
|
||||||
|
return pulled;
|
||||||
|
}
|
||||||
|
self.bag.pull().unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
use axum::{
|
|
||||||
body::Bytes,
|
|
||||||
extract::{FromRequest, Request, rejection::BytesRejection},
|
|
||||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use axum_extra::headers::Mime;
|
|
||||||
use bytes::{BufMut, BytesMut};
|
|
||||||
use core::fmt::Display;
|
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
|
||||||
|
|
||||||
const CBOR_CONTENT_TYPE: &str = "application/cbor";
|
|
||||||
const PLAIN_CONTENT_TYPE: &str = "text/plain";
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub struct Cbor<T>(pub T);
|
|
||||||
|
|
||||||
impl<T> Cbor<T> {
|
|
||||||
pub const fn new(t: T) -> Self {
|
|
||||||
Self(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S> FromRequest<S> for Cbor<T>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned,
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = CborRejection;
|
|
||||||
|
|
||||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
if !cbor_content_type(req.headers()) {
|
|
||||||
return Err(CborRejection::MissingCborContentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = Bytes::from_request(req, state).await?;
|
|
||||||
Ok(Self(ciborium::from_reader::<T, _>(&*bytes)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IntoResponse for Cbor<T>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
// Extracted into separate fn so it's only compiled once for all T.
|
|
||||||
fn make_response(buf: BytesMut, ser_result: Result<(), CborRejection>) -> Response {
|
|
||||||
match ser_result {
|
|
||||||
Ok(()) => (
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(CBOR_CONTENT_TYPE),
|
|
||||||
)],
|
|
||||||
buf.freeze(),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
Err(err) => err.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a small initial capacity of 128 bytes like serde_json::to_vec
|
|
||||||
// https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
|
|
||||||
let mut buf = BytesMut::with_capacity(128).writer();
|
|
||||||
let res = ciborium::into_writer(&self.0, &mut buf)
|
|
||||||
.map_err(|err| CborRejection::SerdeRejection(err.to_string()));
|
|
||||||
make_response(buf.into_inner(), res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CborRejection {
|
|
||||||
MissingCborContentType,
|
|
||||||
BytesRejection(BytesRejection),
|
|
||||||
DeserializeRejection(String),
|
|
||||||
SerdeRejection(String),
|
|
||||||
}
|
|
||||||
impl<T: Display> From<ciborium::de::Error<T>> for CborRejection {
|
|
||||||
fn from(value: ciborium::de::Error<T>) -> Self {
|
|
||||||
Self::SerdeRejection(match value {
|
|
||||||
ciborium::de::Error::Io(err) => format!("i/o: {err}"),
|
|
||||||
ciborium::de::Error::Syntax(offset) => format!("syntax error at {offset}"),
|
|
||||||
ciborium::de::Error::Semantic(offset, err) => format!(
|
|
||||||
"semantic parse: {err}{}",
|
|
||||||
offset
|
|
||||||
.map(|offset| format!(" at {offset}"))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
),
|
|
||||||
ciborium::de::Error::RecursionLimitExceeded => {
|
|
||||||
String::from("the input caused serde to recurse too much")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BytesRejection> for CborRejection {
|
|
||||||
fn from(value: BytesRejection) -> Self {
|
|
||||||
Self::BytesRejection(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for CborRejection {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
match self {
|
|
||||||
CborRejection::MissingCborContentType => (
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(PLAIN_CONTENT_TYPE),
|
|
||||||
)],
|
|
||||||
String::from("missing cbor content type"),
|
|
||||||
),
|
|
||||||
CborRejection::BytesRejection(err) => (
|
|
||||||
err.status(),
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(PLAIN_CONTENT_TYPE),
|
|
||||||
)],
|
|
||||||
format!("bytes rejection: {}", err.body_text()),
|
|
||||||
),
|
|
||||||
CborRejection::SerdeRejection(err) => (
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(PLAIN_CONTENT_TYPE),
|
|
||||||
)],
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
CborRejection::DeserializeRejection(err) => (
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(PLAIN_CONTENT_TYPE),
|
|
||||||
)],
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cbor_content_type(headers: &HeaderMap) -> bool {
|
|
||||||
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(content_type) = content_type.to_str() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(mime) = content_type.parse::<Mime>() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
mime.type_() == "application"
|
|
||||||
&& (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor"))
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,32 @@
|
||||||
use core::{fmt::Display, num::NonZeroU8, ops::Not};
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::{
|
||||||
|
fmt::Display,
|
||||||
|
num::NonZeroU8,
|
||||||
|
ops::{Deref, Not},
|
||||||
|
};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::{Aura, AuraTitle, Auras},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village},
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
modifier::Modifier,
|
|
||||||
player::{PlayerId, RoleChange},
|
player::{PlayerId, RoleChange},
|
||||||
role::{
|
role::{
|
||||||
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
||||||
|
|
@ -41,7 +59,7 @@ pub struct Character {
|
||||||
player_id: PlayerId,
|
player_id: PlayerId,
|
||||||
identity: CharacterIdentity,
|
identity: CharacterIdentity,
|
||||||
role: Role,
|
role: Role,
|
||||||
modifier: Option<Modifier>,
|
auras: Auras,
|
||||||
died_to: Option<DiedTo>,
|
died_to: Option<DiedTo>,
|
||||||
role_changes: Vec<RoleChange>,
|
role_changes: Vec<RoleChange>,
|
||||||
}
|
}
|
||||||
|
|
@ -58,19 +76,20 @@ impl Character {
|
||||||
},
|
},
|
||||||
}: Identification,
|
}: Identification,
|
||||||
role: Role,
|
role: Role,
|
||||||
|
auras: Vec<Aura>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
Some(Self {
|
Some(Self {
|
||||||
role,
|
role,
|
||||||
|
player_id,
|
||||||
|
died_to: None,
|
||||||
|
auras: Auras::new(auras),
|
||||||
|
role_changes: Vec::new(),
|
||||||
identity: CharacterIdentity {
|
identity: CharacterIdentity {
|
||||||
character_id: CharacterId::new(),
|
character_id: CharacterId::new(),
|
||||||
name,
|
name,
|
||||||
pronouns,
|
pronouns,
|
||||||
number: number?,
|
number: number?,
|
||||||
},
|
},
|
||||||
player_id,
|
|
||||||
modifier: None,
|
|
||||||
died_to: None,
|
|
||||||
role_changes: Vec::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,13 +190,6 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn alignment(&self) -> Alignment {
|
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
|
||||||
return Alignment::Wolves;
|
|
||||||
}
|
|
||||||
self.role.alignment()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn elder_reveal(&mut self) {
|
pub fn elder_reveal(&mut self) {
|
||||||
if let Role::Elder {
|
if let Role::Elder {
|
||||||
woken_for_reveal, ..
|
woken_for_reveal, ..
|
||||||
|
|
@ -187,6 +199,18 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn purge_expired_auras(&mut self, village: &Village) -> Box<[Aura]> {
|
||||||
|
self.auras.purge_expired(village)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auras(&self) -> &[Aura] {
|
||||||
|
self.auras.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auras_mut(&mut self) -> &mut [Aura] {
|
||||||
|
self.auras.list_mut()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
||||||
let mut role = new_role.title_to_role_excl_apprentice();
|
let mut role = new_role.title_to_role_excl_apprentice();
|
||||||
core::mem::swap(&mut role, &mut self.role);
|
core::mem::swap(&mut role, &mut self.role);
|
||||||
|
|
@ -224,9 +248,9 @@ impl Character {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mason_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
fn mason_prompts(&self, village: &Village) -> Result<Vec<ActionPrompt>> {
|
||||||
if !self.role.wakes(village) {
|
if !self.role.wakes(village) {
|
||||||
return Ok(Box::new([]));
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let (recruits, recruits_available) = match &self.role {
|
let (recruits, recruits_available) = match &self.role {
|
||||||
Role::MasonLeader {
|
Role::MasonLeader {
|
||||||
|
|
@ -272,18 +296,43 @@ impl Character {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
/// Returns a copy of this character with their role replaced
|
||||||
if self.mason_leader().is_ok() {
|
/// in a read-only type
|
||||||
return self.mason_prompts(village);
|
pub fn as_role(&self, role: Role) -> AsCharacter {
|
||||||
|
let mut char = self.clone();
|
||||||
|
char.role = role;
|
||||||
|
AsCharacter(char)
|
||||||
}
|
}
|
||||||
if !self.alive() || !self.role.wakes(village) {
|
|
||||||
return Ok(Box::new([]));
|
pub fn apply_aura(&mut self, aura: Aura) {
|
||||||
|
self.auras.add(aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_aura(&mut self, aura: AuraTitle) {
|
||||||
|
self.auras.remove_aura(aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||||
|
let mut prompts = Vec::new();
|
||||||
|
if self.mason_leader().is_ok() {
|
||||||
|
// add them here so masons wake up even with a dead leader
|
||||||
|
prompts.append(&mut self.mason_prompts(village)?);
|
||||||
}
|
}
|
||||||
let night = match village.time() {
|
let night = match village.time() {
|
||||||
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
Ok(Box::new([match &self.role {
|
if night == 0 && self.auras.list().contains(&Aura::Traitor) {
|
||||||
|
log::info!("adding traitor prompt for {}", self.identity());
|
||||||
|
prompts.push(ActionPrompt::TraitorIntro {
|
||||||
|
character_id: self.identity(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !self.alive() || !self.role.wakes(village) {
|
||||||
|
return Ok(prompts.into_boxed_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.role {
|
||||||
Role::Empath { cursed: true }
|
Role::Empath { cursed: true }
|
||||||
| Role::Diseased
|
| Role::Diseased
|
||||||
| Role::Weightlifter
|
| Role::Weightlifter
|
||||||
|
|
@ -298,11 +347,11 @@ impl Character {
|
||||||
woken_for_reveal: true,
|
woken_for_reveal: true,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| Role::Villager => return Ok(Box::new([])),
|
| Role::Villager => {}
|
||||||
|
|
||||||
Role::Insomniac => ActionPrompt::Insomniac {
|
Role::Insomniac => prompts.push(ActionPrompt::Insomniac {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
},
|
}),
|
||||||
|
|
||||||
Role::Scapegoat { redeemed: true } => {
|
Role::Scapegoat { redeemed: true } => {
|
||||||
let mut dead = village.dead_characters();
|
let mut dead = village.dead_characters();
|
||||||
|
|
@ -311,44 +360,47 @@ impl Character {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title()))
|
.find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title()))
|
||||||
{
|
{
|
||||||
ActionPrompt::RoleChange {
|
prompts.push(ActionPrompt::RoleChange {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
new_role: pr,
|
new_role: pr,
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Role::Seer => ActionPrompt::Seer {
|
Role::Bloodletter => prompts.push(ActionPrompt::Bloodletter {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_villagers(),
|
||||||
|
marked: None,
|
||||||
|
}),
|
||||||
|
Role::Seer => prompts.push(ActionPrompt::Seer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Arcanist => ActionPrompt::Arcanist {
|
Role::Arcanist => prompts.push(ActionPrompt::Arcanist {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: (None, None),
|
marked: (None, None),
|
||||||
},
|
}),
|
||||||
Role::Protector {
|
Role::Protector {
|
||||||
last_protected: Some(last_protected),
|
last_protected: Some(last_protected),
|
||||||
} => ActionPrompt::Protector {
|
} => prompts.push(ActionPrompt::Protector {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
targets: village.living_players_excluding(*last_protected),
|
targets: village.living_players_excluding(*last_protected),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Protector {
|
Role::Protector {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
} => ActionPrompt::Protector {
|
} => prompts.push(ActionPrompt::Protector {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
targets: village.living_players_excluding(self.character_id()),
|
targets: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Apprentice(role) => {
|
Role::Apprentice(role) => match village.time() {
|
||||||
let current_night = match village.time() {
|
GameTime::Day { number: _ } => {}
|
||||||
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
GameTime::Night {
|
||||||
GameTime::Night { number } => number,
|
number: current_night,
|
||||||
};
|
} => {
|
||||||
return Ok(village
|
if village
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| c.role_title() == *role)
|
.filter(|c| c.role_title() == *role)
|
||||||
|
|
@ -357,48 +409,45 @@ impl Character {
|
||||||
GameTime::Day { number } => number.get() + 1 >= current_night,
|
GameTime::Day { number } => number.get() + 1 >= current_night,
|
||||||
GameTime::Night { number } => number + 1 >= current_night,
|
GameTime::Night { number } => number + 1 >= current_night,
|
||||||
})
|
})
|
||||||
.then(|| ActionPrompt::RoleChange {
|
{
|
||||||
|
prompts.push(ActionPrompt::RoleChange {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
new_role: *role,
|
new_role: *role,
|
||||||
})
|
});
|
||||||
.into_iter()
|
|
||||||
.collect());
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Role::Elder {
|
Role::Elder {
|
||||||
knows_on_night,
|
knows_on_night,
|
||||||
woken_for_reveal: false,
|
woken_for_reveal: false,
|
||||||
..
|
..
|
||||||
} => {
|
} => match village.time() {
|
||||||
let current_night = match village.time() {
|
GameTime::Day { number: _ } => {}
|
||||||
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
GameTime::Night { number } => {
|
||||||
GameTime::Night { number } => number,
|
if number >= knows_on_night.get() {
|
||||||
};
|
prompts.push(ActionPrompt::ElderReveal {
|
||||||
return Ok((current_night >= knows_on_night.get())
|
|
||||||
.then_some({
|
|
||||||
ActionPrompt::ElderReveal {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.collect());
|
|
||||||
}
|
}
|
||||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
},
|
||||||
|
Role::Militia { targeted: None } => prompts.push(ActionPrompt::Militia {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
Role::Werewolf => prompts.push(ActionPrompt::WolfPackKill {
|
||||||
living_villagers: village.living_players(),
|
living_villagers: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
Role::AlphaWolf { killed: None } => prompts.push(ActionPrompt::AlphaWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_villagers: village.living_players_excluding(self.character_id()),
|
living_villagers: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::DireWolf {
|
Role::DireWolf {
|
||||||
last_blocked: Some(last_blocked),
|
last_blocked: Some(last_blocked),
|
||||||
} => ActionPrompt::DireWolf {
|
} => prompts.push(ActionPrompt::DireWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village
|
living_players: village
|
||||||
.living_players_excluding(self.character_id())
|
.living_players_excluding(self.character_id())
|
||||||
|
|
@ -406,137 +455,124 @@ impl Character {
|
||||||
.filter(|c| c.character_id != *last_blocked)
|
.filter(|c| c.character_id != *last_blocked)
|
||||||
.collect(),
|
.collect(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::DireWolf { .. } => ActionPrompt::DireWolf {
|
Role::DireWolf { .. } => prompts.push(ActionPrompt::DireWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
Role::Shapeshifter { shifted_into: None } => prompts.push(ActionPrompt::Shapeshifter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
},
|
}),
|
||||||
Role::Gravedigger => {
|
Role::Gravedigger => {
|
||||||
let dead = village.dead_targets();
|
let dead = village.dead_targets();
|
||||||
if dead.is_empty() {
|
if !dead.is_empty() {
|
||||||
return Ok(Box::new([]));
|
prompts.push(ActionPrompt::Gravedigger {
|
||||||
}
|
|
||||||
ActionPrompt::Gravedigger {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
dead_players: village.dead_targets(),
|
dead_players: village.dead_targets(),
|
||||||
marked: None,
|
marked: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
Role::Hunter { target } => prompts.push(ActionPrompt::Hunter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||||
living_players: village.living_players_excluding(prev_target.character_id),
|
living_players: village.living_players_excluding(prev_target.character_id),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||||
living_players: village.living_players(),
|
living_players: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: None,
|
previous: None,
|
||||||
living_players: village.living_players(),
|
living_players: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Adjudicator => ActionPrompt::Adjudicator {
|
Role::Adjudicator => prompts.push(ActionPrompt::Adjudicator {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::PowerSeer => ActionPrompt::PowerSeer {
|
Role::PowerSeer => prompts.push(ActionPrompt::PowerSeer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Mortician => ActionPrompt::Mortician {
|
Role::Mortician => {
|
||||||
character_id: self.identity(),
|
|
||||||
dead_players: {
|
|
||||||
let dead = village.dead_targets();
|
let dead = village.dead_targets();
|
||||||
if dead.is_empty() {
|
if !dead.is_empty() {
|
||||||
return Ok(Box::new([]));
|
prompts.push(ActionPrompt::Mortician {
|
||||||
}
|
character_id: self.identity(),
|
||||||
dead
|
dead_players: dead,
|
||||||
},
|
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
});
|
||||||
Role::Beholder => ActionPrompt::Beholder {
|
}
|
||||||
|
}
|
||||||
|
Role::Beholder => prompts.push(ActionPrompt::Beholder {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::MasonLeader { .. } => {
|
Role::MasonLeader { .. } => {
|
||||||
log::error!(
|
log::error!(
|
||||||
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
||||||
);
|
);
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
}
|
||||||
Role::Empath { cursed: false } => ActionPrompt::Empath {
|
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Vindicator => {
|
Role::Vindicator => {
|
||||||
let last_day = match village.time() {
|
if night != 0
|
||||||
GameTime::Day { .. } => {
|
&& let Some(last_day) = NonZeroU8::new(night)
|
||||||
log::error!(
|
&& village
|
||||||
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
|
||||||
);
|
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
|
||||||
GameTime::Night { number } => {
|
|
||||||
if number == 0 {
|
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
|
||||||
NonZeroU8::new(number).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Ok(village
|
|
||||||
.executions_on_day(last_day)
|
.executions_on_day(last_day)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.is_village())
|
.any(|c| c.is_village())
|
||||||
.then(|| ActionPrompt::Vindicator {
|
{
|
||||||
|
prompts.push(ActionPrompt::Vindicator {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
})
|
});
|
||||||
.into_iter()
|
|
||||||
.collect());
|
|
||||||
}
|
}
|
||||||
Role::PyreMaster { .. } => ActionPrompt::PyreMaster {
|
}
|
||||||
|
|
||||||
|
Role::PyreMaster { .. } => prompts.push(ActionPrompt::PyreMaster {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::LoneWolf => ActionPrompt::LoneWolfKill {
|
Role::LoneWolf => prompts.push(ActionPrompt::LoneWolfKill {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
}]))
|
}
|
||||||
|
Ok(prompts.into_boxed_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -548,14 +584,30 @@ impl Character {
|
||||||
self.role.killing_wolf_order()
|
self.role.killing_wolf_order()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn killer(&self) -> Killer {
|
pub fn alignment(&self) -> Alignment {
|
||||||
|
if let Some(alignment) = self.auras.overrides_alignment() {
|
||||||
|
return alignment;
|
||||||
|
}
|
||||||
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
|
return Alignment::Wolves;
|
||||||
|
}
|
||||||
|
self.role.alignment()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn killer(&self) -> Killer {
|
||||||
|
if let Some(killer) = self.auras.overrides_killer() {
|
||||||
|
return killer;
|
||||||
|
}
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return Killer::Killer;
|
return Killer::Killer;
|
||||||
}
|
}
|
||||||
self.role.killer()
|
self.role.killer()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn powerful(&self) -> Powerful {
|
pub fn powerful(&self) -> Powerful {
|
||||||
|
if let Some(powerful) = self.auras.overrides_powerful() {
|
||||||
|
return powerful;
|
||||||
|
}
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return Powerful::Powerful;
|
return Powerful::Powerful;
|
||||||
}
|
}
|
||||||
|
|
@ -751,6 +803,50 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn militia<'a>(&'a self) -> Result<Militia<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &self.role {
|
||||||
|
Role::Militia { targeted } => Ok(Militia(targeted)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Militia,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn militia_mut<'a>(&'a mut self) -> Result<MilitiaMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Militia { targeted } => Ok(MilitiaMut(targeted)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Militia,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn maple_wolf_mut<'a>(&'a mut self) -> Result<MapleWolfMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::MapleWolf { last_kill_on_night } => Ok(MapleWolfMut(last_kill_on_night)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::MapleWolf,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn protector_mut<'a>(&'a mut self) -> Result<ProtectorMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Protector { last_protected } => Ok(ProtectorMut(last_protected)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Protector,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn initial_shown_role(&self) -> RoleTitle {
|
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||||
self.role.initial_shown_role()
|
self.role.initial_shown_role()
|
||||||
}
|
}
|
||||||
|
|
@ -792,6 +888,9 @@ decl_ref_and_mut!(
|
||||||
BlackKnight, BlackKnightMut: Option<DiedTo>;
|
BlackKnight, BlackKnightMut: Option<DiedTo>;
|
||||||
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
||||||
Direwolf, DirewolfMut: Option<CharacterId>;
|
Direwolf, DirewolfMut: Option<CharacterId>;
|
||||||
|
Militia, MilitiaMut: Option<CharacterId>;
|
||||||
|
MapleWolf, MapleWolfMut: u8;
|
||||||
|
Protector, ProtectorMut: Option<CharacterId>;
|
||||||
);
|
);
|
||||||
|
|
||||||
pub struct BlackKnightKill<'a> {
|
pub struct BlackKnightKill<'a> {
|
||||||
|
|
@ -834,3 +933,13 @@ impl MasonLeaderMut<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AsCharacter(Character);
|
||||||
|
|
||||||
|
impl Deref for AsCharacter {
|
||||||
|
type Target = Character;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{fmt::Debug, num::NonZeroU8};
|
use core::{fmt::Debug, num::NonZeroU8};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
#[cfg(feature = "server")]
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
use core::fmt::Display;
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -62,6 +73,8 @@ pub enum GameError {
|
||||||
NightNeedsNext,
|
NightNeedsNext,
|
||||||
#[error("night zero actions can only be obtained on night zero")]
|
#[error("night zero actions can only be obtained on night zero")]
|
||||||
NotNightZero,
|
NotNightZero,
|
||||||
|
#[error("this action cannot happen on night zero")]
|
||||||
|
CannotHappenOnNightZero,
|
||||||
#[error("wolves intro in progress")]
|
#[error("wolves intro in progress")]
|
||||||
WolvesIntroInProgress,
|
WolvesIntroInProgress,
|
||||||
#[error("a game is still ongoing")]
|
#[error("a game is still ongoing")]
|
||||||
|
|
@ -84,157 +97,12 @@ pub enum GameError {
|
||||||
MissingTime(GameTime),
|
MissingTime(GameTime),
|
||||||
#[error("no previous during day")]
|
#[error("no previous during day")]
|
||||||
NoPreviousDuringDay,
|
NoPreviousDuringDay,
|
||||||
#[error("server error: {0}")]
|
#[error("militia already spent")]
|
||||||
ServerError(#[from] ServerError),
|
MilitiaSpent,
|
||||||
}
|
#[error("this role doesn't mark anyone")]
|
||||||
|
RoleDoesntMark,
|
||||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
#[error("cannot shapeshift on a non-shapeshifter prompt")]
|
||||||
pub enum DatabaseError {
|
ShapeshiftingIsForShapeshifters,
|
||||||
#[error("user already exists")]
|
#[error("must select a target")]
|
||||||
UserAlreadyExists,
|
MustSelectTarget,
|
||||||
#[error("password hashing error: {0}")]
|
|
||||||
PasswordHashError(String),
|
|
||||||
#[error("sqlx error: {0}")]
|
|
||||||
SqlxError(String),
|
|
||||||
#[error("not found")]
|
|
||||||
NotFound,
|
|
||||||
#[error("serde_json: {0}")]
|
|
||||||
SerdeJson(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl From<serde_json::Error> for DatabaseError {
|
|
||||||
fn from(value: serde_json::Error) -> Self {
|
|
||||||
Self::SerdeJson(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl axum::response::IntoResponse for DatabaseError {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
|
|
||||||
use crate::cbor::Cbor;
|
|
||||||
|
|
||||||
(
|
|
||||||
match self {
|
|
||||||
DatabaseError::UserAlreadyExists => StatusCode::BAD_REQUEST,
|
|
||||||
DatabaseError::NotFound => StatusCode::NOT_FOUND,
|
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
},
|
|
||||||
Cbor(self),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl From<sqlx::Error> for DatabaseError {
|
|
||||||
fn from(err: sqlx::Error) -> Self {
|
|
||||||
match err {
|
|
||||||
sqlx::Error::RowNotFound => Self::NotFound,
|
|
||||||
_ => Self::SqlxError(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl From<argon2::password_hash::Error> for DatabaseError {
|
|
||||||
fn from(err: argon2::password_hash::Error) -> Self {
|
|
||||||
Self::PasswordHashError(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
|
||||||
pub enum ServerError {
|
|
||||||
#[error("database error: {0}")]
|
|
||||||
DatabaseError(DatabaseError),
|
|
||||||
#[error("invalid credentials")]
|
|
||||||
InvalidCredentials,
|
|
||||||
#[error("token expired")]
|
|
||||||
ExpiredToken,
|
|
||||||
#[error("internal server error: {0}")]
|
|
||||||
InternalServerError(String),
|
|
||||||
#[error("connection error")]
|
|
||||||
ConnectionError,
|
|
||||||
#[error("invalid request: {0}")]
|
|
||||||
InvalidRequest(String),
|
|
||||||
#[error("not found")]
|
|
||||||
NotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: Into<DatabaseError>> From<I> for ServerError {
|
|
||||||
fn from(value: I) -> Self {
|
|
||||||
let database_err: DatabaseError = value.into();
|
|
||||||
if let DatabaseError::NotFound = &database_err {
|
|
||||||
return Self::NotFound;
|
|
||||||
}
|
|
||||||
Self::DatabaseError(database_err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GameError> for ServerError {
|
|
||||||
fn from(value: GameError) -> Self {
|
|
||||||
Self::InvalidRequest(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl<T: Display> From<ciborium::de::Error<T>> for ServerError {
|
|
||||||
fn from(_: ciborium::de::Error<T>) -> Self {
|
|
||||||
Self::InvalidRequest(String::from("could not decode request"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl axum::response::IntoResponse for ServerError {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
|
|
||||||
use crate::cbor::Cbor;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
ServerError::ExpiredToken => {
|
|
||||||
(StatusCode::UNAUTHORIZED, Cbor(ServerError::ExpiredToken)).into_response()
|
|
||||||
}
|
|
||||||
ServerError::NotFound | ServerError::DatabaseError(DatabaseError::NotFound) => {
|
|
||||||
(StatusCode::NOT_FOUND, Cbor(ServerError::NotFound)).into_response()
|
|
||||||
}
|
|
||||||
ServerError::DatabaseError(DatabaseError::UserAlreadyExists) => (
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
Cbor(ServerError::InvalidRequest(String::from("username taken"))),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
ServerError::DatabaseError(err) => {
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
let error_id = Uuid::new_v4();
|
|
||||||
log::error!("database error[{error_id}]: {err}");
|
|
||||||
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
Cbor(ServerError::InternalServerError(format!(
|
|
||||||
"internal server error. error id: {error_id}"
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
ServerError::InvalidCredentials => (
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
Cbor(ServerError::InvalidCredentials),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
ServerError::InternalServerError(_) => {
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Cbor(self)).into_response()
|
|
||||||
}
|
|
||||||
ServerError::ConnectionError => {
|
|
||||||
(StatusCode::BAD_REQUEST, Cbor(ServerError::ConnectionError)).into_response()
|
|
||||||
}
|
|
||||||
ServerError::InvalidRequest(reason) => (
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
Cbor(ServerError::InvalidRequest(reason)),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,28 @@
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Village, night::changes::ChangesLookup},
|
game::{
|
||||||
|
Village,
|
||||||
|
night::changes::{ChangesLookup, NightChange},
|
||||||
|
},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -22,10 +39,26 @@ pub enum KillOutcome {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KillOutcome {
|
impl KillOutcome {
|
||||||
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
pub fn apply_to_village(
|
||||||
|
self,
|
||||||
|
village: &mut Village,
|
||||||
|
recorded_changes: Option<&mut Vec<NightChange>>,
|
||||||
|
) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
KillOutcome::Single(character_id, died_to) => {
|
KillOutcome::Single(character_id, died_to) => {
|
||||||
village.character_by_id_mut(character_id)?.kill(died_to);
|
village
|
||||||
|
.character_by_id_mut(character_id)?
|
||||||
|
.kill(died_to.clone());
|
||||||
|
if let DiedTo::Militia { killer, .. } = died_to
|
||||||
|
&& let Some(existing) = village
|
||||||
|
.character_by_id_mut(killer)?
|
||||||
|
.militia_mut()?
|
||||||
|
.replace(character_id)
|
||||||
|
{
|
||||||
|
log::error!("militia kill after already recording a kill on {existing}");
|
||||||
|
return Err(GameError::MilitiaSpent);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
KillOutcome::Guarding {
|
KillOutcome::Guarding {
|
||||||
|
|
@ -38,15 +71,22 @@ impl KillOutcome {
|
||||||
// check if guardian exists before we mutably borrow killer, which would
|
// check if guardian exists before we mutably borrow killer, which would
|
||||||
// prevent us from borrowing village to check after.
|
// prevent us from borrowing village to check after.
|
||||||
village.character_by_id(guardian)?;
|
village.character_by_id(guardian)?;
|
||||||
village
|
let guardian_kill = DiedTo::GuardianProtecting {
|
||||||
.character_by_id_mut(original_killer)?
|
|
||||||
.kill(DiedTo::GuardianProtecting {
|
|
||||||
night,
|
night,
|
||||||
source: guardian,
|
source: guardian,
|
||||||
protecting: original_target,
|
protecting: original_target,
|
||||||
protecting_from: original_killer,
|
protecting_from: original_killer,
|
||||||
protecting_from_cause: Box::new(original_kill.clone()),
|
protecting_from_cause: Box::new(original_kill.clone()),
|
||||||
|
};
|
||||||
|
if let Some(recorded_changes) = recorded_changes {
|
||||||
|
recorded_changes.push(NightChange::Kill {
|
||||||
|
target: original_killer,
|
||||||
|
died_to: guardian_kill.clone(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
village
|
||||||
|
.character_by_id_mut(original_killer)?
|
||||||
|
.kill(guardian_kill);
|
||||||
village.character_by_id_mut(guardian)?.kill(original_kill);
|
village.character_by_id_mut(guardian)?.kill(original_kill);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +122,7 @@ fn resolve_protection(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_kill(
|
pub fn resolve_kill(
|
||||||
changes: &mut ChangesLookup<'_>,
|
changes: &ChangesLookup<'_>,
|
||||||
target: CharacterId,
|
target: CharacterId,
|
||||||
died_to: &DiedTo,
|
died_to: &DiedTo,
|
||||||
night: u8,
|
night: u8,
|
||||||
|
|
@ -93,7 +133,7 @@ pub fn resolve_kill(
|
||||||
night,
|
night,
|
||||||
starves_if_fails: true,
|
starves_if_fails: true,
|
||||||
} = died_to
|
} = died_to
|
||||||
&& let Some(protection) = changes.protected_take(target)
|
&& let Some(protection) = changes.protected(target)
|
||||||
{
|
{
|
||||||
return Ok(Some(
|
return Ok(Some(
|
||||||
resolve_protection(*source, died_to, target, &protection, *night).unwrap_or(
|
resolve_protection(*source, died_to, target, &protection, *night).unwrap_or(
|
||||||
|
|
@ -109,7 +149,7 @@ pub fn resolve_kill(
|
||||||
{
|
{
|
||||||
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||||
|
|
||||||
match changes.protected_take(target) {
|
match changes.protected(target) {
|
||||||
Some(protection) => {
|
Some(protection) => {
|
||||||
return Ok(resolve_protection(
|
return Ok(resolve_protection(
|
||||||
killing_wolf.character_id(),
|
killing_wolf.character_id(),
|
||||||
|
|
@ -122,7 +162,7 @@ pub fn resolve_kill(
|
||||||
None => {
|
None => {
|
||||||
// Wolf kill went through -- can kill shifter
|
// Wolf kill went through -- can kill shifter
|
||||||
return Ok(Some(KillOutcome::Single(
|
return Ok(Some(KillOutcome::Single(
|
||||||
*ss_source,
|
ss_source,
|
||||||
DiedTo::Shapeshift {
|
DiedTo::Shapeshift {
|
||||||
into: target,
|
into: target,
|
||||||
night: *night,
|
night: *night,
|
||||||
|
|
@ -132,7 +172,7 @@ pub fn resolve_kill(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let protection = match changes.protected_take(target) {
|
let protection = match changes.protected(target) {
|
||||||
Some(prot) => prot,
|
Some(prot) => prot,
|
||||||
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
||||||
};
|
};
|
||||||
|
|
@ -147,7 +187,7 @@ pub fn resolve_kill(
|
||||||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||||
original_target: target,
|
original_target: target,
|
||||||
original_kill: died_to.clone(),
|
original_kill: died_to.clone(),
|
||||||
guardian: source,
|
guardian: *source,
|
||||||
night: NonZeroU8::new(night).unwrap(),
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
})),
|
})),
|
||||||
Protection::Guardian {
|
Protection::Guardian {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
mod kill;
|
mod kill;
|
||||||
pub mod night;
|
pub mod night;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
@ -10,7 +24,6 @@ use core::{
|
||||||
ops::{Deref, Range, RangeBounds},
|
ops::{Deref, Range, RangeBounds},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use rand::{Rng, seq::SliceRandom};
|
use rand::{Rng, seq::SliceRandom};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -21,7 +34,6 @@ use crate::{
|
||||||
night::{Night, ServerAction},
|
night::{Night, ServerAction},
|
||||||
story::{DayDetail, GameActions, GameStory, NightDetails},
|
story::{DayDetail, GameActions, GameStory, NightDetails},
|
||||||
},
|
},
|
||||||
id::GameId,
|
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification,
|
CharacterState, Identification,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
|
|
@ -38,8 +50,6 @@ type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
id: GameId,
|
|
||||||
started_at: DateTime<Utc>,
|
|
||||||
history: GameStory,
|
history: GameStory,
|
||||||
state: GameState,
|
state: GameState,
|
||||||
}
|
}
|
||||||
|
|
@ -48,36 +58,12 @@ impl Game {
|
||||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||||
let village = Village::new(players, settings)?;
|
let village = Village::new(players, settings)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: GameId::new(),
|
|
||||||
started_at: Utc::now(),
|
|
||||||
history: GameStory::new(village.clone()),
|
history: GameStory::new(village.clone()),
|
||||||
state: GameState::Night {
|
state: GameState::Night {
|
||||||
night: Night::new(village)?,
|
night: Night::new(village)?,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
#[cfg(feature = "server")]
|
|
||||||
pub const fn new_from_parts(
|
|
||||||
id: GameId,
|
|
||||||
started_at: DateTime<Utc>,
|
|
||||||
history: GameStory,
|
|
||||||
state: GameState,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
started_at,
|
|
||||||
history,
|
|
||||||
state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn game_id(&self) -> GameId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn started_at(&self) -> DateTime<Utc> {
|
|
||||||
self.started_at
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn village(&self) -> &Village {
|
pub const fn village(&self) -> &Village {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
|
|
@ -86,6 +72,15 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn village_mut(&mut self) -> &mut Village {
|
||||||
|
match &mut self.state {
|
||||||
|
GameState::Day { village, marked: _ } => village,
|
||||||
|
GameState::Night { night } => night.village_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
||||||
match (&mut self.state, message) {
|
match (&mut self.state, message) {
|
||||||
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
|
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
|
||||||
|
|
@ -116,7 +111,6 @@ impl Game {
|
||||||
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
||||||
let time = village.time();
|
let time = village.time();
|
||||||
if let Some(outcome) = village.execute(marked)? {
|
if let Some(outcome) = village.execute(marked)? {
|
||||||
log::warn!("adding to history for {}", village.time());
|
|
||||||
self.history.add(
|
self.history.add(
|
||||||
village.time(),
|
village.time(),
|
||||||
GameActions::DayDetails(
|
GameActions::DayDetails(
|
||||||
|
|
@ -126,7 +120,6 @@ impl Game {
|
||||||
return Ok(ServerToHostMessage::GameOver(outcome));
|
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||||
}
|
}
|
||||||
let night = Night::new(village.clone())?;
|
let night = Night::new(village.clone())?;
|
||||||
log::warn!("adding to history for {time}");
|
|
||||||
self.history.add(
|
self.history.add(
|
||||||
time,
|
time,
|
||||||
GameActions::DayDetails(
|
GameActions::DayDetails(
|
||||||
|
|
@ -176,13 +169,13 @@ impl Game {
|
||||||
Ok(_) => self.process(HostGameMessage::GetState),
|
Ok(_) => self.process(HostGameMessage::GetState),
|
||||||
Err(GameError::NightOver) => {
|
Err(GameError::NightOver) => {
|
||||||
let changes = night.collect_changes()?;
|
let changes = night.collect_changes()?;
|
||||||
let village = night.village().with_night_changes(&changes)?;
|
let (village, recorded_changes) =
|
||||||
log::warn!("adding to history for {}", night.village().time());
|
night.village().with_night_changes(&changes)?;
|
||||||
self.history.add(
|
self.history.add(
|
||||||
night.village().time(),
|
night.village().time(),
|
||||||
GameActions::NightDetails(NightDetails::new(
|
GameActions::NightDetails(NightDetails::new(
|
||||||
&night.used_actions(),
|
&night.used_actions(),
|
||||||
changes,
|
recorded_changes,
|
||||||
)),
|
)),
|
||||||
)?;
|
)?;
|
||||||
self.state = GameState::Day {
|
self.state = GameState::Day {
|
||||||
|
|
@ -296,6 +289,15 @@ pub enum GameOver {
|
||||||
WolvesWin,
|
WolvesWin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for GameOver {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
GameOver::VillageWins => f.write_str("village wins"),
|
||||||
|
GameOver::WolvesWin => f.write_str("wolves win"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Pool<T>
|
pub struct Pool<T>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,25 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
pub mod changes;
|
pub mod changes;
|
||||||
|
mod next;
|
||||||
mod process;
|
mod process;
|
||||||
|
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::Extract;
|
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -14,13 +28,11 @@ use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
GameTime, Village,
|
GameTime, Village,
|
||||||
kill::{self},
|
kill::{self, KillOutcome},
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
story::NightChoice,
|
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult, Visits},
|
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||||
player::Protection,
|
role::RoleTitle,
|
||||||
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, RoleBlock, RoleTitle},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum BlockResolvedOutcome {
|
enum BlockResolvedOutcome {
|
||||||
|
|
@ -47,7 +59,8 @@ impl From<ActionComplete> for ResponseOutcome {
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
fn unless(&self) -> Option<Unless> {
|
fn unless(&self) -> Option<Unless> {
|
||||||
match &self {
|
match &self {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -60,7 +73,11 @@ impl ActionPrompt {
|
||||||
..
|
..
|
||||||
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +154,8 @@ impl ActionPrompt {
|
||||||
..
|
..
|
||||||
} => Some(Unless::TargetBlocked(*marked)),
|
} => Some(Unless::TargetBlocked(*marked)),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill { marked: None, .. }
|
ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
| ActionPrompt::Seer { marked: None, .. }
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
| ActionPrompt::Protector { marked: None, .. }
|
| ActionPrompt::Protector { marked: None, .. }
|
||||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||||
|
|
@ -181,6 +199,15 @@ impl Default for ActionComplete {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn night_sort_order(
|
||||||
|
left_prompt: &ActionPrompt,
|
||||||
|
right_prompt: &ActionPrompt,
|
||||||
|
) -> core::cmp::Ordering {
|
||||||
|
left_prompt
|
||||||
|
.partial_cmp(right_prompt)
|
||||||
|
.unwrap_or(core::cmp::Ordering::Equal)
|
||||||
|
}
|
||||||
|
|
||||||
enum Unless {
|
enum Unless {
|
||||||
TargetBlocked(CharacterId),
|
TargetBlocked(CharacterId),
|
||||||
TargetsBlocked(CharacterId, CharacterId),
|
TargetsBlocked(CharacterId, CharacterId),
|
||||||
|
|
@ -238,15 +265,29 @@ pub struct Night {
|
||||||
night: u8,
|
night: u8,
|
||||||
action_queue: VecDeque<ActionPrompt>,
|
action_queue: VecDeque<ActionPrompt>,
|
||||||
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
|
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
|
||||||
|
start_of_night_changes: Vec<NightChange>,
|
||||||
night_state: NightState,
|
night_state: NightState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Night {
|
impl Night {
|
||||||
pub fn new(village: Village) -> Result<Self> {
|
pub fn new(mut village: Village) -> Result<Self> {
|
||||||
let night = match village.time() {
|
let night = match village.time() {
|
||||||
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
|
let mut start_of_night_changes = Self::start_of_night_changes(&village, night);
|
||||||
|
// purge expired auras
|
||||||
|
{
|
||||||
|
let village_clone = village.clone();
|
||||||
|
for char in village.characters_mut() {
|
||||||
|
for expired_aura in char.purge_expired_auras(&village_clone) {
|
||||||
|
start_of_night_changes.push(NightChange::LostAura {
|
||||||
|
character: char.character_id(),
|
||||||
|
aura: expired_aura,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filter = if village.executed_known_elder() {
|
let filter = if village.executed_known_elder() {
|
||||||
// there is a lynched elder, remove villager PRs from the prompts
|
// there is a lynched elder, remove villager PRs from the prompts
|
||||||
|
|
@ -265,12 +306,58 @@ impl Night {
|
||||||
.flatten()
|
.flatten()
|
||||||
.chain(village.wolf_pack_kill())
|
.chain(village.wolf_pack_kill())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
action_queue.sort_by(|left_prompt, right_prompt| {
|
action_queue.sort_by(night_sort_order);
|
||||||
left_prompt
|
|
||||||
.partial_cmp(right_prompt)
|
let mut action_queue = VecDeque::from({
|
||||||
.unwrap_or(core::cmp::Ordering::Equal)
|
// insert actions for role-changed roles
|
||||||
|
let mut expanded_queue = Vec::new();
|
||||||
|
let mut role_changes = Vec::new();
|
||||||
|
// here we replace the role change prompts with the prompt they *would* have gotten
|
||||||
|
// (if any). if they wouldn't get a prompt, just add the role change prompt back in
|
||||||
|
for action in action_queue {
|
||||||
|
match &action {
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id,
|
||||||
|
new_role,
|
||||||
|
} => {
|
||||||
|
let char = village.character_by_id(character_id.character_id)?;
|
||||||
|
let as_role = char.as_role(new_role.title_to_role_excl_apprentice());
|
||||||
|
let prompts = as_role.night_action_prompts(&village)?;
|
||||||
|
if prompts.is_empty() {
|
||||||
|
// they wouldn't get a prompt alongside the role change, so just add
|
||||||
|
// the role change prompt back in
|
||||||
|
expanded_queue.push(action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
role_changes.push((character_id.character_id, *new_role));
|
||||||
|
for prompt in prompts {
|
||||||
|
expanded_queue.push(prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
expanded_queue.push(action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expanded_queue.sort_by(night_sort_order);
|
||||||
|
let mut expanded_queue_with_role_changes =
|
||||||
|
Vec::with_capacity(expanded_queue.len() + role_changes.len());
|
||||||
|
for prompt in expanded_queue {
|
||||||
|
if let Some(char) = prompt.character_id()
|
||||||
|
&& let Some((_, role)) = role_changes.iter().find(|(c, _)| *c == char)
|
||||||
|
{
|
||||||
|
expanded_queue_with_role_changes.push(ActionPrompt::RoleChange {
|
||||||
|
character_id: village.character_by_id(char)?.identity(),
|
||||||
|
new_role: *role,
|
||||||
|
});
|
||||||
|
expanded_queue_with_role_changes.push(prompt);
|
||||||
|
} else {
|
||||||
|
expanded_queue_with_role_changes.push(prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expanded_queue_with_role_changes
|
||||||
});
|
});
|
||||||
let mut action_queue = VecDeque::from(action_queue);
|
|
||||||
|
|
||||||
if night == 0 {
|
if night == 0 {
|
||||||
action_queue.push_front(ActionPrompt::WolvesIntro {
|
action_queue.push_front(ActionPrompt::WolvesIntro {
|
||||||
|
|
@ -314,6 +401,7 @@ impl Night {
|
||||||
village,
|
village,
|
||||||
night_state,
|
night_state,
|
||||||
action_queue,
|
action_queue,
|
||||||
|
start_of_night_changes,
|
||||||
used_actions: Vec::new(),
|
used_actions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -336,77 +424,37 @@ impl Night {
|
||||||
// for prompt in action_queue {
|
// for prompt in action_queue {
|
||||||
while let Some(prompt) = action_queue.pop_front() {
|
while let Some(prompt) = action_queue.pop_front() {
|
||||||
log::warn!("prompt: {:?}", prompt.title());
|
log::warn!("prompt: {:?}", prompt.title());
|
||||||
let (wolf_id, prompt) = match prompt {
|
match prompt {
|
||||||
ActionPrompt::WolvesIntro { mut wolves } => {
|
ActionPrompt::WolvesIntro { mut wolves } => {
|
||||||
if let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting) {
|
if let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting) {
|
||||||
w.1 = reverting_into;
|
w.1 = reverting_into;
|
||||||
}
|
}
|
||||||
new_queue.push_back(ActionPrompt::WolvesIntro { wolves });
|
new_queue.push_back(ActionPrompt::WolvesIntro { wolves });
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
if let Some(char_id) = other.character_id()
|
||||||
|
&& char_id == reverting
|
||||||
|
&& !matches!(other.title(), ActionPromptTitle::RoleChange)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionPrompt::Shapeshifter { character_id } => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::Shapeshifter { character_id },
|
|
||||||
),
|
|
||||||
ActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
living_villagers,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
living_villagers,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
other => {
|
|
||||||
new_queue.push_back(other);
|
new_queue.push_back(other);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if wolf_id != reverting {
|
|
||||||
new_queue.push_back(prompt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new_queue
|
new_queue
|
||||||
}
|
}
|
||||||
|
|
||||||
/// changes that require no input (such as hunter firing)
|
/// changes from the beginning of the night that require no input (such as hunter firing)
|
||||||
fn automatic_changes(&self) -> Vec<NightChange> {
|
fn start_of_night_changes(village: &Village, night: u8) -> Vec<NightChange> {
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
let night = match NonZeroU8::new(self.night) {
|
let night = match NonZeroU8::new(night) {
|
||||||
Some(night) => night,
|
Some(night) => night,
|
||||||
None => return changes,
|
None => return changes,
|
||||||
};
|
};
|
||||||
if !self.village.executed_known_elder() {
|
if !village.executed_known_elder() {
|
||||||
self.village
|
village
|
||||||
.dead_characters()
|
.dead_characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||||
|
|
@ -486,13 +534,7 @@ impl Night {
|
||||||
// role change associated with the shapeshift
|
// role change associated with the shapeshift
|
||||||
self.action_queue.push_front(last_prompt);
|
self.action_queue.push_front(last_prompt);
|
||||||
}
|
}
|
||||||
log::warn!(
|
|
||||||
"next prompts: {:?}",
|
|
||||||
self.action_queue
|
|
||||||
.iter()
|
|
||||||
.map(ActionPrompt::title)
|
|
||||||
.collect::<Box<[_]>>()
|
|
||||||
);
|
|
||||||
*current_result = CurrentResult::None;
|
*current_result = CurrentResult::None;
|
||||||
*current_changes = Vec::new();
|
*current_changes = Vec::new();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -510,9 +552,13 @@ impl Night {
|
||||||
if !matches!(self.night_state, NightState::Complete) {
|
if !matches!(self.night_state, NightState::Complete) {
|
||||||
return Err(GameError::NotEndOfNight);
|
return Err(GameError::NotEndOfNight);
|
||||||
}
|
}
|
||||||
let mut all_changes = self.automatic_changes();
|
Ok(self.current_changes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_changes(&self) -> Box<[NightChange]> {
|
||||||
|
let mut all_changes = self.start_of_night_changes.clone();
|
||||||
all_changes.append(&mut self.changes_from_actions().into_vec());
|
all_changes.append(&mut self.changes_from_actions().into_vec());
|
||||||
Ok(all_changes.into_boxed_slice())
|
all_changes.into_boxed_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_mason_recruit(
|
fn apply_mason_recruit(
|
||||||
|
|
@ -542,7 +588,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
|
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<Option<ActionResult>> {
|
||||||
if let Some(kill_target) = self
|
if let Some(kill_target) = self
|
||||||
.changes_from_actions()
|
.changes_from_actions()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -566,7 +612,7 @@ impl Night {
|
||||||
_ => false,
|
_ => false,
|
||||||
}) {
|
}) {
|
||||||
// there is protection, so the kill doesn't happen -> no shapeshift
|
// there is protection, so the kill doesn't happen -> no shapeshift
|
||||||
return Ok(());
|
return Ok(Some(ActionResult::ShiftFailed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.changes_from_actions().into_iter().any(|c| {
|
if self.changes_from_actions().into_iter().any(|c| {
|
||||||
|
|
@ -617,7 +663,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.action_queue = new_queue;
|
self.action_queue = new_queue;
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn page(&self) -> Option<usize> {
|
pub const fn page(&self) -> Option<usize> {
|
||||||
|
|
@ -631,8 +677,8 @@ impl Night {
|
||||||
match &mut self.night_state {
|
match &mut self.night_state {
|
||||||
NightState::Active { current_result, .. } => match current_result {
|
NightState::Active { current_result, .. } => match current_result {
|
||||||
CurrentResult::None => self.received_response(ActionResponse::Continue),
|
CurrentResult::None => self.received_response(ActionResponse::Continue),
|
||||||
CurrentResult::Result(ActionResult::Continue)
|
CurrentResult::GoBackToSleepAfterShown { .. }
|
||||||
| CurrentResult::GoBackToSleepAfterShown { .. }
|
| CurrentResult::Result(ActionResult::Continue)
|
||||||
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
|
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
|
||||||
Err(GameError::NightNeedsNext)
|
Err(GameError::NightNeedsNext)
|
||||||
}
|
}
|
||||||
|
|
@ -647,7 +693,24 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_current_result(&mut self, result: CurrentResult) -> Result<()> {
|
||||||
|
match &mut self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt: _,
|
||||||
|
current_result,
|
||||||
|
..
|
||||||
|
} => *current_result = result,
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
||||||
|
if let ActionResponse::ContinueToResult = &resp
|
||||||
|
&& let Some(result) = self.current_result()
|
||||||
|
{
|
||||||
|
return Ok(ServerAction::Result(result.clone()));
|
||||||
|
}
|
||||||
match self.received_response_with_role_blocks(resp)? {
|
match self.received_response_with_role_blocks(resp)? {
|
||||||
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
|
|
@ -665,18 +728,18 @@ impl Night {
|
||||||
NightState::Complete => Err(GameError::NightOver),
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
},
|
},
|
||||||
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
||||||
match &mut self.night_state {
|
self.set_current_result(result.clone().into())?;
|
||||||
NightState::Active {
|
|
||||||
current_prompt: _,
|
|
||||||
current_result,
|
|
||||||
..
|
|
||||||
} => *current_result = result.clone().into(),
|
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
|
||||||
};
|
|
||||||
if let NightChange::Shapeshift { source, .. } = &change {
|
if let NightChange::Shapeshift { source, .. } = &change {
|
||||||
// needs to be resolved _now_ so that the target can be woken
|
// needs to be resolved _now_ so that the target can be woken
|
||||||
// for the role change with the wolves
|
// for the role change with the wolves
|
||||||
self.apply_shapeshift(source)?;
|
if let Some(result) = self.apply_shapeshift(source)? {
|
||||||
|
if let NightState::Active { current_result, .. } = &mut self.night_state {
|
||||||
|
*current_result = CurrentResult::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(ServerAction::Result(result));
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ServerAction::Result(
|
return Ok(ServerAction::Result(
|
||||||
self.action_queue
|
self.action_queue
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -716,6 +779,45 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn outcome_consecutive_same_player_no_sleep(
|
||||||
|
&self,
|
||||||
|
outcome: ResponseOutcome,
|
||||||
|
) -> ResponseOutcome {
|
||||||
|
let same_char = self
|
||||||
|
.current_character_id()
|
||||||
|
.and_then(|curr| {
|
||||||
|
self.action_queue
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.map(|n| n.character_id() == Some(curr))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
match (outcome, same_char) {
|
||||||
|
(ResponseOutcome::PromptUpdate(p), _) => ResponseOutcome::PromptUpdate(p),
|
||||||
|
(
|
||||||
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
) => ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::Continue,
|
||||||
|
change,
|
||||||
|
}),
|
||||||
|
(act, _) => act,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn received_response_consecutive_same_player_no_sleep(
|
||||||
|
&self,
|
||||||
|
resp: ActionResponse,
|
||||||
|
) -> Result<ResponseOutcome> {
|
||||||
|
Ok(self.outcome_consecutive_same_player_no_sleep(
|
||||||
|
self.received_response_consecutive_wolves_dont_sleep(resp)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn received_response_consecutive_wolves_dont_sleep(
|
fn received_response_consecutive_wolves_dont_sleep(
|
||||||
&self,
|
&self,
|
||||||
resp: ActionResponse,
|
resp: ActionResponse,
|
||||||
|
|
@ -743,7 +845,11 @@ impl Night {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (self.process(resp)?, current_wolfy, next_wolfy) {
|
match (
|
||||||
|
self.received_response_with_auras(resp)?,
|
||||||
|
current_wolfy,
|
||||||
|
next_wolfy,
|
||||||
|
) {
|
||||||
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
||||||
(
|
(
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -775,7 +881,7 @@ impl Night {
|
||||||
&self,
|
&self,
|
||||||
resp: ActionResponse,
|
resp: ActionResponse,
|
||||||
) -> Result<BlockResolvedOutcome> {
|
) -> Result<BlockResolvedOutcome> {
|
||||||
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
|
match self.received_response_consecutive_same_player_no_sleep(resp)? {
|
||||||
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
||||||
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
||||||
match self
|
match self
|
||||||
|
|
@ -828,12 +934,18 @@ impl Night {
|
||||||
&self.village
|
&self.village
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn village_mut(&mut self) -> &mut Village {
|
||||||
|
&mut self.village
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn current_result(&self) -> Option<&ActionResult> {
|
pub const fn current_result(&self) -> Option<&ActionResult> {
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_result: CurrentResult::Result(current_result),
|
current_result: CurrentResult::Result(current_result),
|
||||||
..
|
..
|
||||||
} => Some(¤t_result),
|
} => Some(current_result),
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
|
current_result: CurrentResult::GoBackToSleepAfterShown { .. },
|
||||||
..
|
..
|
||||||
|
|
@ -864,36 +976,7 @@ impl Night {
|
||||||
current_prompt,
|
current_prompt,
|
||||||
current_result: _,
|
current_result: _,
|
||||||
..
|
..
|
||||||
} => match current_prompt {
|
} => current_prompt.character_id(),
|
||||||
ActionPrompt::Insomniac { character_id, .. }
|
|
||||||
| ActionPrompt::LoneWolfKill { character_id, .. }
|
|
||||||
| ActionPrompt::ElderReveal { character_id }
|
|
||||||
| ActionPrompt::RoleChange { character_id, .. }
|
|
||||||
| ActionPrompt::Seer { character_id, .. }
|
|
||||||
| ActionPrompt::Protector { character_id, .. }
|
|
||||||
| ActionPrompt::Arcanist { character_id, .. }
|
|
||||||
| ActionPrompt::Gravedigger { character_id, .. }
|
|
||||||
| ActionPrompt::Hunter { character_id, .. }
|
|
||||||
| ActionPrompt::Militia { character_id, .. }
|
|
||||||
| ActionPrompt::MapleWolf { character_id, .. }
|
|
||||||
| ActionPrompt::Guardian { character_id, .. }
|
|
||||||
| ActionPrompt::Shapeshifter { character_id }
|
|
||||||
| ActionPrompt::AlphaWolf { character_id, .. }
|
|
||||||
| ActionPrompt::Adjudicator { character_id, .. }
|
|
||||||
| ActionPrompt::PowerSeer { character_id, .. }
|
|
||||||
| ActionPrompt::Mortician { character_id, .. }
|
|
||||||
| ActionPrompt::Beholder { character_id, .. }
|
|
||||||
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
|
|
||||||
| ActionPrompt::Empath { character_id, .. }
|
|
||||||
| ActionPrompt::Vindicator { character_id, .. }
|
|
||||||
| ActionPrompt::PyreMaster { character_id, .. }
|
|
||||||
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
|
||||||
|
|
||||||
ActionPrompt::WolvesIntro { wolves: _ }
|
|
||||||
| ActionPrompt::MasonsWake { .. }
|
|
||||||
| ActionPrompt::WolfPackKill { .. }
|
|
||||||
| ActionPrompt::CoverOfDarkness => None,
|
|
||||||
},
|
|
||||||
NightState::Complete => None,
|
NightState::Complete => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -903,6 +986,11 @@ impl Night {
|
||||||
.and_then(|id| self.village.character_by_id(id).ok())
|
.and_then(|id| self.village.character_by_id(id).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_character_mut(&mut self) -> Option<&mut Character> {
|
||||||
|
self.current_character_id()
|
||||||
|
.and_then(|id| self.village.character_by_id_mut(id).ok())
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn complete(&self) -> bool {
|
pub const fn complete(&self) -> bool {
|
||||||
matches!(self.night_state, NightState::Complete)
|
matches!(self.night_state, NightState::Complete)
|
||||||
}
|
}
|
||||||
|
|
@ -914,78 +1002,41 @@ impl Night {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::should_implement_trait)]
|
pub fn append_change(&mut self, change: NightChange) -> Result<()> {
|
||||||
pub fn next(&mut self) -> Result<()> {
|
match &mut self.night_state {
|
||||||
match &self.night_state {
|
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt,
|
current_changes, ..
|
||||||
current_result: CurrentResult::Result(ActionResult::Continue),
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
self.used_actions.push((
|
current_changes.push(change);
|
||||||
current_prompt.clone(),
|
Ok(())
|
||||||
ActionResult::Continue,
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
NightState::Active {
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
current_prompt,
|
|
||||||
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.used_actions.push((
|
|
||||||
current_prompt.clone(),
|
|
||||||
ActionResult::GoBackToSleep,
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
NightState::Active {
|
|
||||||
current_result: CurrentResult::Result(_),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// needs Continue, not Next
|
|
||||||
return Err(GameError::AwaitingResponse);
|
|
||||||
}
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt,
|
|
||||||
current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data },
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.used_actions.push((
|
|
||||||
current_prompt.clone(),
|
|
||||||
result_with_data.clone(),
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt: _,
|
|
||||||
current_result: CurrentResult::None,
|
|
||||||
..
|
|
||||||
} => return Err(GameError::AwaitingResponse),
|
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
|
||||||
}
|
|
||||||
if let Some(prompt) = self.action_queue.pop_front() {
|
|
||||||
if let ActionPrompt::Insomniac { character_id } = &prompt
|
|
||||||
&& self.get_visits_for(character_id.character_id).is_empty()
|
|
||||||
{
|
|
||||||
// skip!
|
|
||||||
self.used_actions.pop(); // it will be re-added
|
|
||||||
return self.next();
|
|
||||||
}
|
|
||||||
self.night_state = NightState::Active {
|
|
||||||
current_prompt: prompt,
|
|
||||||
current_result: CurrentResult::None,
|
|
||||||
current_changes: Vec::new(),
|
|
||||||
current_page: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
self.night_state = NightState::Complete;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
/// resolves whether the target [CharacterId] dies tonight with the current
|
||||||
|
/// state of the night and returns the [DiedTo] cause of death
|
||||||
|
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||||
|
let ch = self.current_changes();
|
||||||
|
let mut changes = ChangesLookup::new(&ch);
|
||||||
|
changes.died_to(character_id, self.night, &self.village)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the matching [Character] with the current night's aura changes
|
||||||
|
/// applied
|
||||||
|
fn character_with_current_auras(&self, id: CharacterId) -> Result<Character> {
|
||||||
|
let mut character = self.village.character_by_id(id)?.clone();
|
||||||
|
for aura in self
|
||||||
|
.changes_from_actions()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|c| match c {
|
||||||
|
NightChange::ApplyAura { target, aura, .. } => (target == id).then_some(aura),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
character.apply_aura(aura);
|
||||||
|
}
|
||||||
|
Ok(character)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
||||||
|
|
@ -993,6 +1044,16 @@ impl Night {
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(_, _, act)| act.iter())
|
.flat_map(|(_, _, act)| act.iter())
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.chain(
|
||||||
|
match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_changes, ..
|
||||||
|
} => Some(current_changes.iter().cloned()),
|
||||||
|
NightState::Complete => None,
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1000,6 +1061,9 @@ impl Night {
|
||||||
Visits::new(
|
Visits::new(
|
||||||
self.used_actions
|
self.used_actions
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|(_, result, _)| {
|
||||||
|
!matches!(result, ActionResult::Drunk | ActionResult::RoleBlocked)
|
||||||
|
})
|
||||||
.filter_map(|(prompt, _, _)| match prompt {
|
.filter_map(|(prompt, _, _)| match prompt {
|
||||||
ActionPrompt::Arcanist {
|
ActionPrompt::Arcanist {
|
||||||
character_id,
|
character_id,
|
||||||
|
|
@ -1014,7 +1078,12 @@ impl Night {
|
||||||
.then(|| self.village.killing_wolf().map(|c| c.identity()))
|
.then(|| self.village.killing_wolf().map(|c| c.identity()))
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Seer {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
|
|
@ -1105,7 +1174,9 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => (*marked == visit_char).then(|| character_id.clone()),
|
} => (*marked == visit_char).then(|| character_id.clone()),
|
||||||
|
|
||||||
ActionPrompt::WolfPackKill { marked: None, .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
| ActionPrompt::Arcanist { marked: _, .. }
|
| ActionPrompt::Arcanist { marked: _, .. }
|
||||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
| ActionPrompt::Seer { marked: None, .. }
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,31 @@
|
||||||
use core::ops::Not;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::Extract;
|
use werewolves_macros::Extract;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
|
game::{
|
||||||
|
Village,
|
||||||
|
kill::{self, KillOutcome},
|
||||||
|
},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{RoleBlock, RoleTitle},
|
role::{RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -45,6 +65,38 @@ pub enum NightChange {
|
||||||
empath: CharacterId,
|
empath: CharacterId,
|
||||||
scapegoat: CharacterId,
|
scapegoat: CharacterId,
|
||||||
},
|
},
|
||||||
|
ApplyAura {
|
||||||
|
source: CharacterId,
|
||||||
|
target: CharacterId,
|
||||||
|
aura: Aura,
|
||||||
|
},
|
||||||
|
LostAura {
|
||||||
|
character: CharacterId,
|
||||||
|
aura: Aura,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NightChange {
|
||||||
|
pub const fn target(&self) -> Option<CharacterId> {
|
||||||
|
match self {
|
||||||
|
NightChange::HunterTarget { target, .. }
|
||||||
|
| NightChange::Kill { target, .. }
|
||||||
|
| NightChange::RoleBlock { target, .. }
|
||||||
|
| NightChange::Shapeshift { into: target, .. }
|
||||||
|
| NightChange::Protection { target, .. }
|
||||||
|
| NightChange::MasonRecruit {
|
||||||
|
recruiting: target, ..
|
||||||
|
}
|
||||||
|
| NightChange::EmpathFoundScapegoat {
|
||||||
|
scapegoat: target, ..
|
||||||
|
}
|
||||||
|
| NightChange::ApplyAura { target, .. } => Some(*target),
|
||||||
|
|
||||||
|
NightChange::ElderReveal { .. }
|
||||||
|
| NightChange::RoleChange(..)
|
||||||
|
| NightChange::LostAura { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
||||||
|
|
@ -54,6 +106,67 @@ impl<'a> ChangesLookup<'a> {
|
||||||
Self(changes, Vec::new())
|
Self(changes, Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn collect_remaining(&self) -> Box<[NightChange]> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, c)| self.1.contains(&idx).not().then_some(c))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill_outcomes(&self, night: u8, village: &Village) -> Result<Box<[KillOutcome]>> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| match c {
|
||||||
|
NightChange::Kill { target, died_to } => Some((*target, died_to.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|(kill_target, died_to)| {
|
||||||
|
kill::resolve_kill(self, kill_target, &died_to, night, village)
|
||||||
|
})
|
||||||
|
.filter_map(|result| match result {
|
||||||
|
Ok(Some(outcome)) => Some(Ok(outcome)),
|
||||||
|
Ok(None) => None,
|
||||||
|
Err(err) => Some(Err(err)),
|
||||||
|
})
|
||||||
|
.collect::<Result<Box<[_]>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn died_to(
|
||||||
|
&self,
|
||||||
|
character_id: CharacterId,
|
||||||
|
night: u8,
|
||||||
|
village: &Village,
|
||||||
|
) -> Result<Option<DiedTo>> {
|
||||||
|
let kill_outcomes = self.kill_outcomes(night, village)?;
|
||||||
|
|
||||||
|
Ok(kill_outcomes.into_iter().find_map(|outcome| match outcome {
|
||||||
|
KillOutcome::Single(target, died_to) => (target == character_id).then_some(died_to),
|
||||||
|
KillOutcome::Guarding {
|
||||||
|
original_killer,
|
||||||
|
original_target,
|
||||||
|
original_kill,
|
||||||
|
guardian,
|
||||||
|
night,
|
||||||
|
} => {
|
||||||
|
if original_killer == character_id {
|
||||||
|
Some(DiedTo::GuardianProtecting {
|
||||||
|
source: guardian,
|
||||||
|
protecting: original_target,
|
||||||
|
protecting_from: original_killer,
|
||||||
|
protecting_from_cause: Box::new(original_kill),
|
||||||
|
night,
|
||||||
|
})
|
||||||
|
} else if guardian == character_id {
|
||||||
|
Some(original_kill)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
self.1
|
self.1
|
||||||
|
|
@ -87,7 +200,7 @@ impl<'a> ChangesLookup<'a> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
pub fn protected(&self, target: CharacterId) -> Option<&'a Protection> {
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
self.1
|
self.1
|
||||||
.contains(&idx)
|
.contains(&idx)
|
||||||
|
|
@ -96,20 +209,27 @@ impl<'a> ChangesLookup<'a> {
|
||||||
NightChange::Protection {
|
NightChange::Protection {
|
||||||
target: t,
|
target: t,
|
||||||
protection,
|
protection,
|
||||||
} => (t == target).then_some(protection),
|
} => (*t == target).then_some(protection),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
pub fn shapeshifter(&self) -> Option<CharacterId> {
|
||||||
|
self.shapeshift_change().and_then(|c| match c {
|
||||||
|
NightChange::Shapeshift { source, .. } => Some(source),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shapeshift_change(&self) -> Option<NightChange> {
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
self.1
|
self.1
|
||||||
.contains(&idx)
|
.contains(&idx)
|
||||||
.not()
|
.not()
|
||||||
.then_some(match c {
|
.then_some(match c {
|
||||||
NightChange::Shapeshift { source, .. } => Some(source),
|
NightChange::Shapeshift { .. } => Some(c.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
||||||
|
message::night::{ActionPrompt, ActionResult},
|
||||||
|
role::{RoleBlock, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
impl Night {
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn next(&mut self) -> Result<()> {
|
||||||
|
self.retroactive_role_blocks()?;
|
||||||
|
self.next_state_process_maple_starving()?;
|
||||||
|
|
||||||
|
match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::Result(ActionResult::Continue),
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
ActionResult::Continue,
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
ActionResult::GoBackToSleep,
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_result: CurrentResult::Result(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// needs Continue, not Next
|
||||||
|
return Err(GameError::AwaitingResponse);
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data },
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
result_with_data.clone(),
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt: _,
|
||||||
|
current_result: CurrentResult::None,
|
||||||
|
..
|
||||||
|
} => return Err(GameError::AwaitingResponse),
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
}
|
||||||
|
if let Some(prompt) = self.pull_next_prompt_with_dead_ignore()? {
|
||||||
|
if let ActionPrompt::Insomniac { character_id } = &prompt
|
||||||
|
&& self.get_visits_for(character_id.character_id).is_empty()
|
||||||
|
{
|
||||||
|
// skip!
|
||||||
|
self.used_actions.pop(); // it will be re-added
|
||||||
|
return self.next();
|
||||||
|
}
|
||||||
|
self.night_state = NightState::Active {
|
||||||
|
current_prompt: prompt,
|
||||||
|
current_result: CurrentResult::None,
|
||||||
|
current_changes: Vec::new(),
|
||||||
|
current_page: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.night_state = NightState::Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_state_process_maple_starving(&mut self) -> Result<()> {
|
||||||
|
let (maple_id, target) = match self.current_prompt() {
|
||||||
|
Some((
|
||||||
|
ActionPrompt::MapleWolf {
|
||||||
|
character_id,
|
||||||
|
kill_or_die,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
)) => {
|
||||||
|
if *kill_or_die {
|
||||||
|
(character_id.character_id, *marked)
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) | None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let starve_change = if let Some(night) = NonZeroU8::new(self.night) {
|
||||||
|
NightChange::Kill {
|
||||||
|
target: maple_id,
|
||||||
|
died_to: DiedTo::MapleWolfStarved { night },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(target) = target else {
|
||||||
|
return self.append_change(starve_change);
|
||||||
|
};
|
||||||
|
match self.died_to_tonight(target)? {
|
||||||
|
Some(DiedTo::MapleWolf { source, .. }) => {
|
||||||
|
if source != maple_id {
|
||||||
|
self.append_change(starve_change)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) | None => {
|
||||||
|
self.append_change(starve_change)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retroactive_role_blocks(&mut self) -> Result<()> {
|
||||||
|
let blocks = match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_changes, ..
|
||||||
|
} => current_changes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| match c {
|
||||||
|
NightChange::RoleBlock {
|
||||||
|
target, block_type, ..
|
||||||
|
} => Some((*target, *block_type)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>(),
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
};
|
||||||
|
for (target, block_type) in blocks {
|
||||||
|
match block_type {
|
||||||
|
RoleBlock::Direwolf => self.apply_direwolf_block_retroactively(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_direwolf_block_retroactively(&mut self, target: CharacterId) {
|
||||||
|
self.used_actions
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|(prompt, res, changes)| match prompt.marked() {
|
||||||
|
Some((marked, None)) => (marked == target).then_some((res, changes)),
|
||||||
|
Some((marked1, Some(marked2))) => {
|
||||||
|
(marked1 == target || marked2 == target).then_some((res, changes))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
.for_each(|(result, changes)| {
|
||||||
|
changes.clear();
|
||||||
|
*result = ActionResult::RoleBlocked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pull_next_prompt_with_dead_ignore(&mut self) -> Result<Option<ActionPrompt>> {
|
||||||
|
let has_living_beholder = self
|
||||||
|
.village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.any(|c| matches!(c.role_title(), RoleTitle::Beholder));
|
||||||
|
while let Some(prompt) = self.action_queue.pop_front() {
|
||||||
|
let Some(char_id) = prompt.character_id() else {
|
||||||
|
return Ok(Some(prompt));
|
||||||
|
};
|
||||||
|
match (self.died_to_tonight(char_id)?, has_living_beholder) {
|
||||||
|
(Some(_), false) => {}
|
||||||
|
(Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => {
|
||||||
|
return Ok(Some(prompt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
|
bag::DrunkRoll,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{
|
game::night::{
|
||||||
|
|
@ -64,7 +80,7 @@ impl Night {
|
||||||
.ok_or(GameError::InvalidTarget)?,
|
.ok_or(GameError::InvalidTarget)?,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
_ => Err(GameError::InvalidMessageForGameState),
|
_ => Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ActionResponse::Continue => {
|
ActionResponse::Continue => {
|
||||||
|
|
@ -91,9 +107,28 @@ impl Night {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActionResponse::ContinueToResult => return self.process(ActionResponse::Continue),
|
||||||
};
|
};
|
||||||
|
|
||||||
match current_prompt {
|
match current_prompt {
|
||||||
|
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::ApplyAura {
|
||||||
|
source: character_id.character_id,
|
||||||
|
aura: Aura::Bloodlet { night: self.night },
|
||||||
|
target: *marked,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::LoneWolfKill {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -129,7 +164,7 @@ impl Night {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let alignment = self.village.character_by_id(*marked)?.alignment();
|
let alignment = self.character_with_current_auras(*marked)?.alignment();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Seer(alignment),
|
result: ActionResult::Seer(alignment),
|
||||||
change: None,
|
change: None,
|
||||||
|
|
@ -152,8 +187,8 @@ impl Night {
|
||||||
marked: (Some(marked1), Some(marked2)),
|
marked: (Some(marked1), Some(marked2)),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let same = self.village.character_by_id(*marked1)?.alignment()
|
let same = self.character_with_current_auras(*marked1)?.alignment()
|
||||||
== self.village.character_by_id(*marked2)?.alignment();
|
== self.character_with_current_auras(*marked2)?.alignment();
|
||||||
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
||||||
|
|
@ -164,7 +199,9 @@ impl Night {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
let dig_role = self
|
||||||
|
.character_with_current_auras(*marked)?
|
||||||
|
.gravedigger_dig();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GraveDigger(dig_role),
|
result: ActionResult::GraveDigger(dig_role),
|
||||||
change: None,
|
change: None,
|
||||||
|
|
@ -192,7 +229,7 @@ impl Night {
|
||||||
died_to: DiedTo::Militia {
|
died_to: DiedTo::Militia {
|
||||||
killer: character_id.character_id,
|
killer: character_id.character_id,
|
||||||
night: NonZeroU8::new(self.night)
|
night: NonZeroU8::new(self.night)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
|
@ -206,12 +243,11 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::Kill {
|
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
|
||||||
target: *marked,
|
target: *marked,
|
||||||
died_to: DiedTo::MapleWolf {
|
died_to: DiedTo::MapleWolf {
|
||||||
|
night,
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
starves_if_fails: *kill_or_die,
|
starves_if_fails: *kill_or_die,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -283,7 +319,7 @@ impl Night {
|
||||||
.ok_or(GameError::NoWolves)?
|
.ok_or(GameError::NoWolves)?
|
||||||
.character_id(),
|
.character_id(),
|
||||||
night: NonZeroU8::new(self.night)
|
night: NonZeroU8::new(self.night)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
|
@ -306,7 +342,7 @@ impl Night {
|
||||||
})
|
})
|
||||||
.ok_or(GameError::InvalidTarget)?,
|
.ok_or(GameError::InvalidTarget)?,
|
||||||
}),
|
}),
|
||||||
_ => return Err(GameError::InvalidMessageForGameState),
|
_ => return Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +357,7 @@ impl Night {
|
||||||
died_to: DiedTo::AlphaWolf {
|
died_to: DiedTo::AlphaWolf {
|
||||||
killer: character_id.character_id,
|
killer: character_id.character_id,
|
||||||
night: NonZeroU8::new(self.night)
|
night: NonZeroU8::new(self.night)
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
|
|
@ -345,7 +381,7 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ActionComplete {
|
} => Ok(ActionComplete {
|
||||||
result: ActionResult::Adjudicator {
|
result: ActionResult::Adjudicator {
|
||||||
killer: self.village.character_by_id(*marked)?.killer(),
|
killer: self.character_with_current_auras(*marked)?.killer(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +391,7 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ActionComplete {
|
} => Ok(ActionComplete {
|
||||||
result: ActionResult::PowerSeer {
|
result: ActionResult::PowerSeer {
|
||||||
powerful: self.village.character_by_id(*marked)?.powerful(),
|
powerful: self.character_with_current_auras(*marked)?.powerful(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
|
|
@ -380,9 +416,15 @@ impl Night {
|
||||||
} => {
|
} => {
|
||||||
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
|
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
|
||||||
prompt.matches_beholding(*marked).then_some(result)
|
prompt.matches_beholding(*marked).then_some(result)
|
||||||
}) {
|
}) && self.died_to_tonight(*marked)?.is_some()
|
||||||
|
{
|
||||||
Ok(ActionComplete {
|
Ok(ActionComplete {
|
||||||
result: result.clone(),
|
result: if matches!(result, ActionResult::RoleBlocked | ActionResult::Drunk)
|
||||||
|
{
|
||||||
|
ActionResult::BeholderSawNothing
|
||||||
|
} else {
|
||||||
|
result.clone()
|
||||||
|
},
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
.into())
|
.into())
|
||||||
|
|
@ -470,7 +512,8 @@ impl Night {
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
|
|
||||||
ActionPrompt::Adjudicator { marked: None, .. }
|
ActionPrompt::Bloodletter { marked: None, .. }
|
||||||
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
| ActionPrompt::PowerSeer { marked: None, .. }
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
| ActionPrompt::Mortician { marked: None, .. }
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
| ActionPrompt::Beholder { marked: None, .. }
|
| ActionPrompt::Beholder { marked: None, .. }
|
||||||
|
|
@ -495,7 +538,57 @@ impl Night {
|
||||||
| ActionPrompt::Guardian { marked: None, .. }
|
| ActionPrompt::Guardian { marked: None, .. }
|
||||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
| ActionPrompt::DireWolf { marked: None, .. }
|
| ActionPrompt::DireWolf { marked: None, .. }
|
||||||
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::MustSelectTarget),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn received_response_with_auras(
|
||||||
|
&self,
|
||||||
|
resp: ActionResponse,
|
||||||
|
) -> Result<ResponseOutcome> {
|
||||||
|
let outcome = self.process(resp)?;
|
||||||
|
if matches!(
|
||||||
|
self.current_prompt(),
|
||||||
|
Some((ActionPrompt::TraitorIntro { .. }, _))
|
||||||
|
| Some((ActionPrompt::RoleChange { .. }, _))
|
||||||
|
| Some((ActionPrompt::ElderReveal { .. }, _))
|
||||||
|
) {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
let mut act = match outcome {
|
||||||
|
ResponseOutcome::PromptUpdate(prompt) => {
|
||||||
|
return Ok(ResponseOutcome::PromptUpdate(prompt));
|
||||||
|
}
|
||||||
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::Drunk,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
| ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::RoleBlocked,
|
||||||
|
..
|
||||||
|
}) => return Ok(outcome),
|
||||||
|
ResponseOutcome::ActionComplete(act) => act,
|
||||||
|
};
|
||||||
|
let Some(char) = self.current_character() else {
|
||||||
|
return Ok(ResponseOutcome::ActionComplete(act));
|
||||||
|
};
|
||||||
|
for aura in char.auras() {
|
||||||
|
match aura {
|
||||||
|
Aura::Traitor | Aura::Bloodlet { .. } => continue,
|
||||||
|
Aura::Drunk(bag) => {
|
||||||
|
if bag.peek() == DrunkRoll::Drunk {
|
||||||
|
act.change = None;
|
||||||
|
act.result = ActionResult::Drunk;
|
||||||
|
return Ok(ResponseOutcome::ActionComplete(act));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Aura::Insane => {
|
||||||
|
if let Some(insane_result) = act.result.insane() {
|
||||||
|
act.result = insane_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ResponseOutcome::ActionComplete(act))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
mod settings_role;
|
mod settings_role;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{
|
use core::{
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
|
|
@ -9,10 +23,10 @@ use uuid::Uuid;
|
||||||
use werewolves_macros::{All, ChecksAs, Titles};
|
use werewolves_macros::{All, ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::AuraTitle,
|
||||||
character::Character,
|
character::Character,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::Identification,
|
message::Identification,
|
||||||
modifier::Modifier,
|
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -113,6 +127,8 @@ pub enum SetupRole {
|
||||||
Shapeshifter,
|
Shapeshifter,
|
||||||
#[checks(Category::Wolves)]
|
#[checks(Category::Wolves)]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
Bloodletter,
|
||||||
|
|
||||||
#[checks(Category::Intel)]
|
#[checks(Category::Intel)]
|
||||||
Adjudicator,
|
Adjudicator,
|
||||||
|
|
@ -141,8 +157,49 @@ pub enum SetupRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SetupRoleTitle {
|
impl SetupRoleTitle {
|
||||||
|
pub fn can_assign_aura(&self, aura: AuraTitle) -> bool {
|
||||||
|
if self.into_role().title().wolf() {
|
||||||
|
return match aura {
|
||||||
|
AuraTitle::Traitor | AuraTitle::Bloodlet | AuraTitle::Insane => false,
|
||||||
|
AuraTitle::Drunk => !matches!(self, SetupRoleTitle::Werewolf),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match aura {
|
||||||
|
AuraTitle::Traitor => true,
|
||||||
|
AuraTitle::Drunk => {
|
||||||
|
matches!(
|
||||||
|
self.category(),
|
||||||
|
Category::StartsAsVillager
|
||||||
|
| Category::Defensive
|
||||||
|
| Category::Intel
|
||||||
|
| Category::Offensive
|
||||||
|
) && !matches!(
|
||||||
|
self,
|
||||||
|
Self::Elder
|
||||||
|
| Self::BlackKnight
|
||||||
|
| Self::Diseased
|
||||||
|
| Self::Weightlifter
|
||||||
|
| Self::Insomniac
|
||||||
|
| Self::Mortician
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AuraTitle::Insane => {
|
||||||
|
matches!(self.category(), Category::Intel)
|
||||||
|
&& !matches!(
|
||||||
|
self,
|
||||||
|
Self::MasonLeader
|
||||||
|
| Self::Empath
|
||||||
|
| Self::Insomniac
|
||||||
|
| Self::Mortician
|
||||||
|
| Self::Gravedigger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AuraTitle::Bloodlet => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
|
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
||||||
SetupRoleTitle::Insomniac => Role::Insomniac,
|
SetupRoleTitle::Insomniac => Role::Insomniac,
|
||||||
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
||||||
SetupRoleTitle::Villager => Role::Villager,
|
SetupRoleTitle::Villager => Role::Villager,
|
||||||
|
|
@ -194,6 +251,7 @@ impl SetupRoleTitle {
|
||||||
impl Display for SetupRole {
|
impl Display for SetupRole {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
|
SetupRole::Bloodletter => "Bloodletter",
|
||||||
SetupRole::Insomniac => "Insomniac",
|
SetupRole::Insomniac => "Insomniac",
|
||||||
SetupRole::LoneWolf => "Lone Wolf",
|
SetupRole::LoneWolf => "Lone Wolf",
|
||||||
SetupRole::Villager => "Villager",
|
SetupRole::Villager => "Villager",
|
||||||
|
|
@ -230,6 +288,7 @@ impl Display for SetupRole {
|
||||||
impl SetupRole {
|
impl SetupRole {
|
||||||
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
|
Self::Bloodletter => Role::Bloodletter,
|
||||||
SetupRole::Insomniac => Role::Insomniac,
|
SetupRole::Insomniac => Role::Insomniac,
|
||||||
SetupRole::LoneWolf => Role::LoneWolf,
|
SetupRole::LoneWolf => Role::LoneWolf,
|
||||||
SetupRole::Villager => Role::Villager,
|
SetupRole::Villager => Role::Villager,
|
||||||
|
|
@ -307,6 +366,7 @@ impl From<SetupRole> for RoleTitle {
|
||||||
impl From<RoleTitle> for SetupRole {
|
impl From<RoleTitle> for SetupRole {
|
||||||
fn from(value: RoleTitle) -> Self {
|
fn from(value: RoleTitle) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
RoleTitle::Bloodletter => SetupRole::Bloodletter,
|
||||||
RoleTitle::Insomniac => SetupRole::Insomniac,
|
RoleTitle::Insomniac => SetupRole::Insomniac,
|
||||||
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
||||||
RoleTitle::Villager => SetupRole::Villager,
|
RoleTitle::Villager => SetupRole::Villager,
|
||||||
|
|
@ -359,7 +419,7 @@ impl SlotId {
|
||||||
pub struct SetupSlot {
|
pub struct SetupSlot {
|
||||||
pub slot_id: SlotId,
|
pub slot_id: SlotId,
|
||||||
pub role: SetupRole,
|
pub role: SetupRole,
|
||||||
pub modifiers: Vec<Modifier>,
|
pub auras: Vec<AuraTitle>,
|
||||||
pub assign_to: Option<PlayerId>,
|
pub assign_to: Option<PlayerId>,
|
||||||
pub created_order: u32,
|
pub created_order: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -370,7 +430,7 @@ impl SetupSlot {
|
||||||
created_order,
|
created_order,
|
||||||
assign_to: None,
|
assign_to: None,
|
||||||
role: title.into(),
|
role: title.into(),
|
||||||
modifiers: Vec::new(),
|
auras: Vec::new(),
|
||||||
slot_id: SlotId::new(),
|
slot_id: SlotId::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -380,7 +440,14 @@ impl SetupSlot {
|
||||||
ident: Identification,
|
ident: Identification,
|
||||||
roles_in_game: &[RoleTitle],
|
roles_in_game: &[RoleTitle],
|
||||||
) -> Result<Character, GameError> {
|
) -> Result<Character, GameError> {
|
||||||
Character::new(ident.clone(), self.role.into_role(roles_in_game)?)
|
Character::new(
|
||||||
|
ident.clone(),
|
||||||
|
self.role.into_role(roles_in_game)?,
|
||||||
|
self.auras
|
||||||
|
.into_iter()
|
||||||
|
.map(|aura| aura.into_aura())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -69,11 +83,19 @@ pub enum StoryActionResult {
|
||||||
Mortician(DiedToTitle),
|
Mortician(DiedToTitle),
|
||||||
Insomniac { visits: Box<[CharacterId]> },
|
Insomniac { visits: Box<[CharacterId]> },
|
||||||
Empath { scapegoat: bool },
|
Empath { scapegoat: bool },
|
||||||
|
BeholderSawNothing,
|
||||||
|
BeholderSawEverything,
|
||||||
|
Drunk,
|
||||||
|
ShiftFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionResult {
|
impl StoryActionResult {
|
||||||
pub fn new(result: ActionResult) -> Option<Self> {
|
pub fn new(result: ActionResult) -> Option<Self> {
|
||||||
Some(match result {
|
Some(match result {
|
||||||
|
ActionResult::ShiftFailed => Self::ShiftFailed,
|
||||||
|
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
||||||
|
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
|
||||||
|
ActionResult::Drunk => Self::Drunk,
|
||||||
ActionResult::RoleBlocked => Self::RoleBlocked,
|
ActionResult::RoleBlocked => Self::RoleBlocked,
|
||||||
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
||||||
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
||||||
|
|
@ -183,11 +205,23 @@ pub enum StoryActionPrompt {
|
||||||
Insomniac {
|
Insomniac {
|
||||||
character_id: CharacterId,
|
character_id: CharacterId,
|
||||||
},
|
},
|
||||||
|
Bloodletter {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionPrompt {
|
impl StoryActionPrompt {
|
||||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||||
Some(match prompt {
|
Some(match prompt {
|
||||||
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Bloodletter {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -362,7 +396,9 @@ impl StoryActionPrompt {
|
||||||
character_id: character_id.character_id,
|
character_id: character_id.character_id,
|
||||||
},
|
},
|
||||||
|
|
||||||
ActionPrompt::Protector { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::Protector { .. }
|
||||||
| ActionPrompt::Gravedigger { .. }
|
| ActionPrompt::Gravedigger { .. }
|
||||||
| ActionPrompt::Hunter { .. }
|
| ActionPrompt::Hunter { .. }
|
||||||
| ActionPrompt::Militia { .. }
|
| ActionPrompt::Militia { .. }
|
||||||
|
|
@ -419,7 +455,7 @@ impl GameStory {
|
||||||
village = match actions {
|
village = match actions {
|
||||||
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
||||||
GameActions::NightDetails(night_details) => {
|
GameActions::NightDetails(night_details) => {
|
||||||
village.with_night_changes(&night_details.changes)?
|
village.with_night_changes(&night_details.changes)?.0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -432,7 +468,7 @@ impl GameStory {
|
||||||
village = match actions {
|
village = match actions {
|
||||||
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
||||||
GameActions::NightDetails(night_details) => {
|
GameActions::NightDetails(night_details) => {
|
||||||
village.with_night_changes(&night_details.changes)?
|
village.with_night_changes(&night_details.changes)?.0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if time == at_time {
|
if time == at_time {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
mod apply;
|
mod apply;
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
|
@ -57,7 +71,10 @@ impl Village {
|
||||||
|
|
||||||
pub fn wolf_revert_prompt(&self) -> Option<ActionPrompt> {
|
pub fn wolf_revert_prompt(&self) -> Option<ActionPrompt> {
|
||||||
self.killing_wolf()
|
self.killing_wolf()
|
||||||
.filter(|killing_wolf| RoleTitle::Werewolf != killing_wolf.role_title())
|
.filter(|killing_wolf| {
|
||||||
|
RoleTitle::Werewolf != killing_wolf.role_title()
|
||||||
|
&& !killing_wolf.role_title().killing_wolf()
|
||||||
|
})
|
||||||
.map(|killing_wolf| ActionPrompt::RoleChange {
|
.map(|killing_wolf| ActionPrompt::RoleChange {
|
||||||
character_id: killing_wolf.identity(),
|
character_id: killing_wolf.identity(),
|
||||||
new_role: RoleTitle::Werewolf,
|
new_role: RoleTitle::Werewolf,
|
||||||
|
|
@ -277,6 +294,7 @@ impl Village {
|
||||||
impl RoleTitle {
|
impl RoleTitle {
|
||||||
pub fn title_to_role_excl_apprentice(self) -> Role {
|
pub fn title_to_role_excl_apprentice(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
|
RoleTitle::Bloodletter => Role::Bloodletter,
|
||||||
RoleTitle::Insomniac => Role::Insomniac,
|
RoleTitle::Insomniac => Role::Insomniac,
|
||||||
RoleTitle::LoneWolf => Role::LoneWolf,
|
RoleTitle::LoneWolf => Role::LoneWolf,
|
||||||
RoleTitle::Villager => Role::Villager,
|
RoleTitle::Villager => Role::Villager,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
GameTime, Village, kill,
|
GameTime, Village,
|
||||||
|
kill::{self, KillOutcome},
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
story::DayDetail,
|
story::DayDetail,
|
||||||
},
|
},
|
||||||
|
|
@ -28,16 +44,42 @@ impl Village {
|
||||||
|
|
||||||
Ok(new_village)
|
Ok(new_village)
|
||||||
}
|
}
|
||||||
pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result<Self> {
|
pub fn with_night_changes(
|
||||||
|
&self,
|
||||||
|
all_changes: &[NightChange],
|
||||||
|
) -> Result<(Self, Box<[NightChange]>)> {
|
||||||
let night = match self.time {
|
let night = match self.time {
|
||||||
GameTime::Day { .. } => return Err(GameError::NotNight),
|
GameTime::Day { .. } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
let mut changes = ChangesLookup::new(all_changes);
|
let changes = ChangesLookup::new(all_changes);
|
||||||
|
|
||||||
let mut new_village = self.clone();
|
let mut new_village = self.clone();
|
||||||
|
|
||||||
|
// recorded changes: changes sans failed kills, actions failed due to blocks, etc
|
||||||
|
let mut recorded_changes = all_changes.to_vec();
|
||||||
|
|
||||||
|
// dispose of the current drunk token for every drunk in the village
|
||||||
|
new_village
|
||||||
|
.characters_mut()
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|c| {
|
||||||
|
c.auras_mut().iter_mut().find_map(|a| match a {
|
||||||
|
Aura::Drunk(bag) => Some(bag),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.for_each(|bag| {
|
||||||
|
// dispose of a token
|
||||||
|
let _ = bag.pull();
|
||||||
|
});
|
||||||
|
|
||||||
for change in all_changes {
|
for change in all_changes {
|
||||||
match change {
|
match change {
|
||||||
|
NightChange::ApplyAura { target, aura, .. } => {
|
||||||
|
let target = new_village.character_by_id_mut(*target)?;
|
||||||
|
target.apply_aura(aura.clone());
|
||||||
|
}
|
||||||
NightChange::ElderReveal { elder } => {
|
NightChange::ElderReveal { elder } => {
|
||||||
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||||
}
|
}
|
||||||
|
|
@ -47,29 +89,52 @@ impl Village {
|
||||||
NightChange::HunterTarget { source, target } => {
|
NightChange::HunterTarget { source, target } => {
|
||||||
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
||||||
hunter_character.hunter_mut()?.replace(*target);
|
hunter_character.hunter_mut()?.replace(*target);
|
||||||
if changes.killed(*source).is_some()
|
if changes
|
||||||
&& changes.protected(source).is_none()
|
.died_to(hunter_character.character_id(), night, self)?
|
||||||
&& changes.protected(target).is_none()
|
.is_some()
|
||||||
{
|
&& let Some(kill) = kill::resolve_kill(
|
||||||
new_village
|
&changes,
|
||||||
.character_by_id_mut(*target)
|
*target,
|
||||||
.unwrap()
|
&DiedTo::Hunter {
|
||||||
.kill(DiedTo::Hunter {
|
|
||||||
killer: *source,
|
killer: *source,
|
||||||
night: NonZeroU8::new(night).unwrap(),
|
night: NonZeroU8::new(night)
|
||||||
})
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
|
},
|
||||||
|
night,
|
||||||
|
&new_village,
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::Kill { target, died_to } => {
|
NightChange::Kill { target, died_to } => {
|
||||||
if let Some(kill) =
|
if let Some(kill) = kill::resolve_kill(&changes, *target, died_to, night, self)?
|
||||||
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
|
||||||
{
|
{
|
||||||
kill.apply_to_village(&mut new_village)?;
|
if let KillOutcome::Guarding {
|
||||||
|
guardian,
|
||||||
|
original_kill,
|
||||||
|
..
|
||||||
|
} = &kill
|
||||||
|
{
|
||||||
|
recorded_changes.retain(|c| c != change);
|
||||||
|
recorded_changes.push(NightChange::Kill {
|
||||||
|
target: *guardian,
|
||||||
|
died_to: original_kill.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
||||||
|
if let DiedTo::MapleWolf { source, .. } = died_to
|
||||||
|
&& let Ok(maple) = new_village.character_by_id_mut(*source)
|
||||||
|
{
|
||||||
|
*maple.maple_wolf_mut()? = night;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recorded_changes.retain(|c| c != change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::Shapeshift { source, into } => {
|
NightChange::Shapeshift { source, into } => {
|
||||||
if let Some(target) = changes.wolf_pack_kill_target()
|
if let Some(target) = changes.wolf_pack_kill_target()
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(*target).is_none()
|
||||||
{
|
{
|
||||||
if *target != *into {
|
if *target != *into {
|
||||||
log::error!("shapeshift into({into}) != target({target})");
|
log::error!("shapeshift into({into}) != target({target})");
|
||||||
|
|
@ -82,6 +147,8 @@ impl Village {
|
||||||
night: NonZeroU8::new(night).unwrap(),
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
});
|
});
|
||||||
// role change pushed in [apply_shapeshift]
|
// role change pushed in [apply_shapeshift]
|
||||||
|
} else {
|
||||||
|
recorded_changes.retain(|c| c != change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,9 +176,13 @@ impl Village {
|
||||||
.character_by_id_mut(*source)?
|
.character_by_id_mut(*source)?
|
||||||
.direwolf_mut()?
|
.direwolf_mut()?
|
||||||
.replace(*target);
|
.replace(*target);
|
||||||
|
|
||||||
|
recorded_changes.retain(|c| {
|
||||||
|
matches!(c, NightChange::RoleBlock { .. })
|
||||||
|
|| c.target().map(|t| t == *target).unwrap_or_default().not()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
NightChange::Protection { .. } => {}
|
|
||||||
NightChange::MasonRecruit {
|
NightChange::MasonRecruit {
|
||||||
mason_leader,
|
mason_leader,
|
||||||
recruiting,
|
recruiting,
|
||||||
|
|
@ -123,6 +194,7 @@ impl Village {
|
||||||
tried_recruiting: *recruiting,
|
tried_recruiting: *recruiting,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
recorded_changes.retain(|c| c != change);
|
||||||
} else {
|
} else {
|
||||||
new_village
|
new_village
|
||||||
.character_by_id_mut(*mason_leader)?
|
.character_by_id_mut(*mason_leader)?
|
||||||
|
|
@ -136,6 +208,41 @@ impl Village {
|
||||||
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
||||||
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
||||||
}
|
}
|
||||||
|
NightChange::LostAura { character, aura } => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*character)?
|
||||||
|
.remove_aura(aura.title());
|
||||||
|
}
|
||||||
|
NightChange::Protection { protection, target } => {
|
||||||
|
let target_ident = new_village.character_by_id(*target)?.identity();
|
||||||
|
match protection {
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: true,
|
||||||
|
} => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.guardian_mut()?
|
||||||
|
.replace(PreviousGuardianAction::Guard(target_ident));
|
||||||
|
}
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: false,
|
||||||
|
} => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.guardian_mut()?
|
||||||
|
.replace(PreviousGuardianAction::Protect(target_ident));
|
||||||
|
}
|
||||||
|
Protection::Protector { source } => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.protector_mut()?
|
||||||
|
.replace(*target);
|
||||||
|
}
|
||||||
|
Protection::Vindicator { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// black knights death
|
// black knights death
|
||||||
|
|
@ -179,6 +286,6 @@ impl Village {
|
||||||
if new_village.is_game_over().is_none() {
|
if new_village.is_game_over().is_none() {
|
||||||
new_village.to_day()?;
|
new_village.to_day()?;
|
||||||
}
|
}
|
||||||
Ok(new_village)
|
Ok((new_village, recorded_changes.into_boxed_slice()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{
|
||||||
|
Game, GameSettings, SetupRole,
|
||||||
|
night::changes::{ChangesLookup, NightChange},
|
||||||
|
},
|
||||||
|
game_test::{GameExt, SettingsExt, gen_players, init_log},
|
||||||
|
player::Protection,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shapeshift() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let shapeshifter = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let game = Game::new(&players, settings).unwrap();
|
||||||
|
|
||||||
|
let all_changes = [
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Shapeshift {
|
||||||
|
source: game.character_by_player_id(shapeshifter).character_id(),
|
||||||
|
into: game.character_by_player_id(target).character_id(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let changes = ChangesLookup::new(&all_changes);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(target).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(shapeshifter).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(Some(DiedTo::Shapeshift {
|
||||||
|
into: game.character_by_player_id(target).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap()
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guardian_protect() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let game = Game::new(&players, settings).unwrap();
|
||||||
|
|
||||||
|
let all_changes = [
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Protection {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
guarding: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let changes = ChangesLookup::new(&all_changes);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(target).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(guardian).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guardian_guard() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let game = Game::new(&players, settings).unwrap();
|
||||||
|
|
||||||
|
let all_changes = [
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Protection {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
guarding: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let changes = ChangesLookup::new(&all_changes);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(target).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(guardian).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap()
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(wolf).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(Some(DiedTo::GuardianProtecting {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
protecting: game.character_by_player_id(target).character_id(),
|
||||||
|
protecting_from: game.character_by_player_id(wolf).character_id(),
|
||||||
|
protecting_from_cause: Box::new(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
}),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guardian_protect_from_multiple_attackers() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let militia = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Villager, militia);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let game = Game::new(&players, settings).unwrap();
|
||||||
|
|
||||||
|
let all_changes = [
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Militia {
|
||||||
|
killer: game.character_by_player_id(militia).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Protection {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
guarding: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let changes = ChangesLookup::new(&all_changes);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(target).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(guardian).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guardian_protect_someone_else_unprotected_dies() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Seer, target);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let game = Game::new(&players, settings).unwrap();
|
||||||
|
|
||||||
|
let all_changes = [
|
||||||
|
NightChange::Kill {
|
||||||
|
target: game.character_by_player_id(target).character_id(),
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NightChange::Protection {
|
||||||
|
target: game.living_villager().character_id(),
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
guarding: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let changes = ChangesLookup::new(&all_changes);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(target).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap()
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
changes.died_to(
|
||||||
|
game.character_by_player_id(guardian).character_id(),
|
||||||
|
1,
|
||||||
|
game.village()
|
||||||
|
),
|
||||||
|
Ok(None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
mod changes;
|
||||||
mod night_order;
|
mod night_order;
|
||||||
mod previous;
|
mod previous;
|
||||||
mod revert;
|
mod revert;
|
||||||
|
|
@ -69,9 +84,17 @@ pub trait ActionPromptTitleExt {
|
||||||
fn insomniac(&self);
|
fn insomniac(&self);
|
||||||
fn power_seer(&self);
|
fn power_seer(&self);
|
||||||
fn mortician(&self);
|
fn mortician(&self);
|
||||||
|
fn elder_reveal(&self);
|
||||||
|
fn bloodletter(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
fn bloodletter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Bloodletter);
|
||||||
|
}
|
||||||
|
fn elder_reveal(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::ElderReveal);
|
||||||
|
}
|
||||||
fn mortician(&self) {
|
fn mortician(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::Mortician);
|
assert_eq!(*self, ActionPromptTitle::Mortician);
|
||||||
}
|
}
|
||||||
|
|
@ -164,9 +187,17 @@ pub trait ActionResultExt {
|
||||||
fn adjudicator(&self) -> Killer;
|
fn adjudicator(&self) -> Killer;
|
||||||
fn mortician(&self) -> DiedToTitle;
|
fn mortician(&self) -> DiedToTitle;
|
||||||
fn empath(&self) -> bool;
|
fn empath(&self) -> bool;
|
||||||
|
fn drunk(&self);
|
||||||
|
fn shapeshift_failed(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionResultExt for ActionResult {
|
impl ActionResultExt for ActionResult {
|
||||||
|
fn shapeshift_failed(&self) {
|
||||||
|
assert_eq!(*self, Self::ShiftFailed)
|
||||||
|
}
|
||||||
|
fn drunk(&self) {
|
||||||
|
assert_eq!(*self, Self::Drunk)
|
||||||
|
}
|
||||||
fn empath(&self) -> bool {
|
fn empath(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Empath { scapegoat } => *scapegoat,
|
Self::Empath { scapegoat } => *scapegoat,
|
||||||
|
|
@ -229,7 +260,7 @@ impl ActionResultExt for ActionResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[allow(unused)]
|
||||||
pub trait AlignmentExt {
|
pub trait AlignmentExt {
|
||||||
fn village(&self);
|
fn village(&self);
|
||||||
fn wolves(&self);
|
fn wolves(&self);
|
||||||
|
|
@ -245,6 +276,7 @@ impl AlignmentExt for Alignment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub trait ServerToHostMessageExt {
|
pub trait ServerToHostMessageExt {
|
||||||
fn prompt(self) -> ActionPrompt;
|
fn prompt(self) -> ActionPrompt;
|
||||||
fn result(self) -> ActionResult;
|
fn result(self) -> ActionResult;
|
||||||
|
|
@ -300,6 +332,7 @@ pub trait GameExt {
|
||||||
fn get_state(&mut self) -> ServerToHostMessage;
|
fn get_state(&mut self) -> ServerToHostMessage;
|
||||||
fn next_expect_game_over(&mut self) -> GameOver;
|
fn next_expect_game_over(&mut self) -> GameOver;
|
||||||
fn prev(&mut self) -> ServerToHostMessage;
|
fn prev(&mut self) -> ServerToHostMessage;
|
||||||
|
fn mark_villager(&mut self) -> ActionPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameExt for Game {
|
impl GameExt for Game {
|
||||||
|
|
@ -372,10 +405,15 @@ impl GameExt for Game {
|
||||||
.prompt()
|
.prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mark_villager(&mut self) -> ActionPrompt {
|
||||||
|
self.mark(self.living_villager().character_id())
|
||||||
|
}
|
||||||
|
|
||||||
fn mark_and_check(&mut self, mark: CharacterId) {
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||||
let prompt = self.mark(mark);
|
let prompt = self.mark(mark);
|
||||||
match prompt {
|
match prompt {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::ElderReveal { .. }
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
|
@ -384,7 +422,11 @@ impl GameExt for Game {
|
||||||
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||||
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
|
@ -460,7 +502,8 @@ impl GameExt for Game {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => assert_eq!(marked, mark, "marked character"),
|
} => assert_eq!(marked, mark, "marked character"),
|
||||||
ActionPrompt::Seer { marked: None, .. }
|
ActionPrompt::Bloodletter { marked: None, .. }
|
||||||
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
| ActionPrompt::Adjudicator { marked: None, .. }
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
| ActionPrompt::PowerSeer { marked: None, .. }
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
| ActionPrompt::Mortician { marked: None, .. }
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
|
|
@ -681,18 +724,7 @@ fn yes_wolf_kill_n2() {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
|
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
|
||||||
);
|
);
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
assert!(matches!(
|
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
||||||
.unwrap(),
|
|
||||||
ServerToHostMessage::ActionPrompt(
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
living_villagers: _,
|
|
||||||
marked: _,
|
|
||||||
},
|
|
||||||
0
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -823,7 +855,7 @@ fn big_game_test_based_on_story_test() {
|
||||||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||||
settings.add_and_assign(hunter.0, hunter.1);
|
settings.add_and_assign(hunter.0, hunter.1);
|
||||||
settings.fill_remaining_slots_with_villagers(players.len());
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
#[allow(unused)]
|
||||||
let (
|
let (
|
||||||
werewolf,
|
werewolf,
|
||||||
dire_wolf,
|
dire_wolf,
|
||||||
|
|
@ -907,7 +939,9 @@ fn big_game_test_based_on_story_test() {
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
game.next().title().shapeshifter();
|
game.next().title().shapeshifter();
|
||||||
game.response(ActionResponse::Shapeshift).sleep();
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
|
@ -962,7 +996,6 @@ fn big_game_test_based_on_story_test() {
|
||||||
|
|
||||||
game.next().title().beholder();
|
game.next().title().beholder();
|
||||||
game.mark(game.character_by_player_id(power_seer).character_id());
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
game.r#continue().power_seer();
|
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
@ -1043,7 +1076,6 @@ fn big_game_test_based_on_story_test() {
|
||||||
|
|
||||||
game.next().title().beholder();
|
game.next().title().beholder();
|
||||||
game.mark(game.character_by_player_id(power_seer).character_id());
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
game.r#continue().power_seer();
|
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
@ -1051,21 +1083,29 @@ fn big_game_test_based_on_story_test() {
|
||||||
game.living_villager_excl(protect.player_id())
|
game.living_villager_excl(protect.player_id())
|
||||||
.character_id(),
|
.character_id(),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
game.execute(),
|
|
||||||
ActionPrompt::RoleChange {
|
|
||||||
character_id: game.character_by_player_id(shapeshifter).identity(),
|
|
||||||
new_role: RoleTitle::Werewolf
|
|
||||||
}
|
|
||||||
);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
game.next().title().vindicator();
|
game.execute().title().vindicator();
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().wolf_pack_kill();
|
game.next().title().wolf_pack_kill();
|
||||||
game.mark(game.character_by_player_id(empath).character_id());
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Shapeshift,
|
||||||
|
)))
|
||||||
|
.expect("shapeshift");
|
||||||
|
// game.r#continue().r#continue();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: game.character_by_player_id(empath).identity(),
|
||||||
|
new_role: RoleTitle::Werewolf
|
||||||
|
}
|
||||||
|
);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
|
|
@ -1112,13 +1152,12 @@ fn big_game_test_based_on_story_test() {
|
||||||
|
|
||||||
game.next().title().beholder();
|
game.next().title().beholder();
|
||||||
game.mark(game.character_by_player_id(gravedigger).character_id());
|
game.mark(game.character_by_player_id(gravedigger).character_id());
|
||||||
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
|
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
|
game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
|
||||||
game.execute().title().wolf_pack_kill();
|
game.execute().title().wolf_pack_kill();
|
||||||
game.mark(game.living_villager().character_id());
|
game.mark(game.character_by_player_id(mortician).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
|
|
@ -1128,23 +1167,23 @@ fn big_game_test_based_on_story_test() {
|
||||||
|
|
||||||
game.next().title().arcanist();
|
game.next().title().arcanist();
|
||||||
game.mark(game.character_by_player_id(insomniac).character_id());
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
game.r#continue().arcanist();
|
game.r#continue().arcanist();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().adjudicator();
|
game.next().title().adjudicator();
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
game.r#continue().adjudicator();
|
game.r#continue().adjudicator();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().power_seer();
|
game.next().title().power_seer();
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
game.r#continue().power_seer();
|
game.r#continue().power_seer();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().gravedigger();
|
game.next().title().gravedigger();
|
||||||
game.mark(game.character_by_player_id(empath).character_id());
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Empath));
|
assert_eq!(game.r#continue().gravedigger(), None);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().mortician();
|
game.next().title().mortician();
|
||||||
|
|
@ -1160,7 +1199,7 @@ fn big_game_test_based_on_story_test() {
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().hunter();
|
game.next().title().hunter();
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().insomniac();
|
game.next().title().insomniac();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -213,7 +227,7 @@ fn previous_prompt() {
|
||||||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||||
settings.add_and_assign(hunter.0, hunter.1);
|
settings.add_and_assign(hunter.0, hunter.1);
|
||||||
settings.fill_remaining_slots_with_villagers(players.len());
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
#[allow(unused)]
|
||||||
let (
|
let (
|
||||||
werewolf,
|
werewolf,
|
||||||
dire_wolf,
|
dire_wolf,
|
||||||
|
|
@ -339,7 +353,9 @@ fn previous_prompt() {
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
game.next().title().shapeshifter();
|
game.next().title().shapeshifter();
|
||||||
game.response(ActionResponse::Shapeshift).sleep();
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
|
@ -394,7 +410,6 @@ fn previous_prompt() {
|
||||||
|
|
||||||
game.next().title().beholder();
|
game.next().title().beholder();
|
||||||
game.mark(game.character_by_player_id(power_seer).character_id());
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
game.r#continue().power_seer();
|
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,29 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use crate::{
|
use crate::{
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
},
|
},
|
||||||
message::{CharacterIdentity, night::ActionPrompt},
|
message::night::ActionPrompt,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sole_non_werewolf_wolves_revert() {
|
fn sole_non_werewolf_wolves_revert() {
|
||||||
const REVERTING_WOLVES: &[SetupRole] = &[
|
const REVERTING_WOLVES: &[SetupRole] = &[SetupRole::DireWolf];
|
||||||
SetupRole::DireWolf,
|
|
||||||
SetupRole::LoneWolf,
|
|
||||||
SetupRole::AlphaWolf,
|
|
||||||
SetupRole::Shapeshifter,
|
|
||||||
];
|
|
||||||
init_log();
|
init_log();
|
||||||
for wolf_role in REVERTING_WOLVES {
|
for wolf_role in REVERTING_WOLVES {
|
||||||
let role_title = Into::<RoleTitle>::into(wolf_role.clone());
|
let role_title = Into::<RoleTitle>::into(wolf_role.clone());
|
||||||
|
|
@ -58,12 +67,7 @@ fn sole_non_werewolf_wolves_revert() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wolves_revert_on_werewolf_death() {
|
fn wolves_revert_on_werewolf_death() {
|
||||||
const REVERTING_WOLVES: &[SetupRole] = &[
|
const REVERTING_WOLVES: &[SetupRole] = &[SetupRole::DireWolf];
|
||||||
SetupRole::DireWolf,
|
|
||||||
SetupRole::LoneWolf,
|
|
||||||
SetupRole::AlphaWolf,
|
|
||||||
SetupRole::Shapeshifter,
|
|
||||||
];
|
|
||||||
init_log();
|
init_log();
|
||||||
for wolf_role in REVERTING_WOLVES {
|
for wolf_role in REVERTING_WOLVES {
|
||||||
let role_title = Into::<RoleTitle>::into(wolf_role.clone());
|
let role_title = Into::<RoleTitle>::into(wolf_role.clone());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
role::RoleTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn beholder_appropriate_prompt_position() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let apprentice = players[0].player_id;
|
||||||
|
let beholder = players[1].player_id;
|
||||||
|
let wolf_player_id = players[2].player_id;
|
||||||
|
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::Apprentice {
|
||||||
|
to: Some(RoleTitle::Beholder),
|
||||||
|
},
|
||||||
|
apprentice,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Beholder, beholder);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(beholder).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: game.character_by_player_id(apprentice).identity(),
|
||||||
|
new_role: RoleTitle::Beholder
|
||||||
|
}
|
||||||
|
);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().beholder();
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
init_log,
|
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::ActionPromptTitle,
|
||||||
|
role::Alignment,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -45,7 +58,21 @@ fn beholding_seer() {
|
||||||
|
|
||||||
game.next().title().beholder();
|
game.next().title().beholder();
|
||||||
game.mark(game.character_by_player_id(seer_player_id).character_id());
|
game.mark(game.character_by_player_id(seer_player_id).character_id());
|
||||||
game.r#continue().seer().wolves();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(seer_player_id).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(seer_player_id).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
message::night::ActionPromptTitle,
|
||||||
SettingsExt, gen_players,
|
|
||||||
},
|
|
||||||
message::{
|
|
||||||
host::{HostDayMessage, HostGameMessage},
|
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
|
||||||
},
|
|
||||||
role::Role,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::ActionPromptTitle,
|
||||||
|
role::{Alignment, Killer, Powerful},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lasts_2_nights() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let target = player_ids.next().unwrap();
|
||||||
|
let seer = player_ids.next().unwrap();
|
||||||
|
let adjudicator = player_ids.next().unwrap();
|
||||||
|
let power_seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let bloodletter = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
settings.add_and_assign(SetupRole::Adjudicator, adjudicator);
|
||||||
|
settings.add_and_assign(SetupRole::PowerSeer, power_seer);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Bloodletter, bloodletter);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().bloodletter();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::Killer);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::Powerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(target).auras(),
|
||||||
|
&[Aura::Bloodlet { night: 0 }]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(target).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().bloodletter();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::Killer);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::Powerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(target).auras(),
|
||||||
|
&[Aura::Bloodlet { night: 0 }]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(game.character_by_player_id(bloodletter).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(target).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::NotPowerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(target).auras(), &[]);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_on_wolf_kill_target_prevents_kill() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let direwolf = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
},
|
||||||
|
scapegoat,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id();
|
||||||
|
game.mark(scapegoat_char_id);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(scapegoat_char_id);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(scapegoat).died_to().cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_on_guardian_target_prevents_the_visit() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let direwolf = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
},
|
||||||
|
scapegoat,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().guardian();
|
||||||
|
let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id();
|
||||||
|
game.mark(scapegoat_char_id);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(scapegoat_char_id);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(guardian)
|
||||||
|
.guardian()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,32 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt,
|
||||||
SettingsExt, gen_players,
|
gen_players,
|
||||||
},
|
},
|
||||||
message::{
|
message::{
|
||||||
host::{HostDayMessage, HostGameMessage},
|
host::{HostDayMessage, HostGameMessage},
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
night::{ActionPrompt, ActionPromptTitle},
|
||||||
},
|
},
|
||||||
role::Role,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::ActionPromptTitle,
|
||||||
role::{Alignment, Role},
|
role::{Alignment, Role},
|
||||||
};
|
};
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -85,20 +99,15 @@ fn doesnt_die_first_try_night_knows() {
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
game.execute().title().wolf_pack_kill();
|
game.execute().title().elder_reveal();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
game.mark_and_check(elder.character_id());
|
game.mark_and_check(elder.character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
game.next(),
|
|
||||||
ActionPrompt::ElderReveal {
|
|
||||||
character_id: elder.identity()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
@ -222,7 +231,10 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
game.execute().title().wolf_pack_kill();
|
game.execute().title().elder_reveal();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
game.mark(villagers.next().unwrap());
|
game.mark(villagers.next().unwrap());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
|
@ -235,14 +247,6 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
|
||||||
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
game.next(),
|
|
||||||
ActionPrompt::ElderReveal {
|
|
||||||
character_id: game.character_by_player_id(elder_player_id).identity()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||||
game_test::{
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
message::night::{ActionPromptTitle, ActionResult},
|
||||||
},
|
|
||||||
message::night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
|
||||||
role::Role,
|
role::Role,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -11,7 +28,7 @@ use crate::{
|
||||||
host::{HostGameMessage, HostNightMessage},
|
host::{HostGameMessage, HostNightMessage},
|
||||||
night::{ActionPromptTitle, ActionResponse},
|
night::{ActionPromptTitle, ActionResponse},
|
||||||
},
|
},
|
||||||
role::{PreviousGuardianAction, Role},
|
role::{PreviousGuardianAction, Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -201,3 +218,54 @@ fn cannot_visit_previous_nights_guard_target() {
|
||||||
Err(GameError::InvalidTarget)
|
Err(GameError::InvalidTarget)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protects_from_militia() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let guardian = player_ids.next().unwrap();
|
||||||
|
let militia = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Militia, militia);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
let mut villagers = game
|
||||||
|
.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| c.alive() && matches!(c.role().title(), RoleTitle::Villager));
|
||||||
|
let protected = villagers.next().unwrap();
|
||||||
|
game.mark(protected.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(villagers.next().unwrap().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().militia();
|
||||||
|
game.mark(protected.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protected.player_id())
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(militia).role().clone(),
|
||||||
|
Role::Militia { targeted: None }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
|
||||||
error::GameError,
|
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
},
|
},
|
||||||
message::{
|
message::night::ActionPromptTitle,
|
||||||
host::{HostGameMessage, HostNightMessage},
|
role::Role,
|
||||||
night::{ActionPromptTitle, ActionResponse},
|
|
||||||
},
|
|
||||||
role::{PreviousGuardianAction, Role},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,23 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
game::{Game, GameSettings, SetupRole},
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
game_test::{
|
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
|
||||||
},
|
|
||||||
message::night::{ActionPromptTitle, Visits},
|
message::night::{ActionPromptTitle, Visits},
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
|
bag::DrunkBag,
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::ActionPromptTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maple_starves() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(maple).died_to().cloned(),
|
||||||
|
Some(DiedTo::MapleWolfStarved {
|
||||||
|
night: NonZeroU8::new(3).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maple_last_eat_counter_increments() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(maple).died_to().cloned(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drunk_maple_doesnt_eat() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.village_mut()
|
||||||
|
.characters_mut()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.player_id() == maple)
|
||||||
|
.unwrap()
|
||||||
|
.apply_aura(Aura::Drunk(DrunkBag::all_drunk()));
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.village()
|
||||||
|
.character_by_id(maple_kill)
|
||||||
|
.unwrap()
|
||||||
|
.died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.village()
|
||||||
|
.character_by_id(maple_kill)
|
||||||
|
.unwrap()
|
||||||
|
.died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(maple).died_to().cloned(),
|
||||||
|
Some(DiedTo::MapleWolfStarved {
|
||||||
|
night: NonZeroU8::new(3).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -229,3 +243,63 @@ fn masons_wake_even_if_leader_died() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn masons_get_go_back_to_sleep() {
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let mason = player_ids.next().unwrap();
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let beholder = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::MasonLeader {
|
||||||
|
recruits_available: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
mason,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
},
|
||||||
|
scapegoat,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Beholder, beholder);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_leader_recruit();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::{num::NonZeroU8, ops::Deref};
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
|
message::night::ActionPromptTitle,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spent_shot() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let militia = players[0].player_id;
|
||||||
|
let target_wolf = players[1].player_id;
|
||||||
|
let other_wolf = players[2].player_id;
|
||||||
|
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Militia, militia);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, target_wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, other_wolf);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().militia();
|
||||||
|
game.mark(game.character_by_player_id(target_wolf).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(militia)
|
||||||
|
.militia()
|
||||||
|
.unwrap()
|
||||||
|
.deref()
|
||||||
|
.clone(),
|
||||||
|
Some(game.character_by_player_id(target_wolf).character_id())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(target_wolf).died_to().cloned(),
|
||||||
|
Some(DiedTo::Militia {
|
||||||
|
killer: game.character_by_player_id(militia).character_id(),
|
||||||
|
night: NonZeroU8::new(1).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
mod apprentice;
|
||||||
mod beholder;
|
mod beholder;
|
||||||
mod black_knight;
|
mod black_knight;
|
||||||
|
mod bloodletter;
|
||||||
|
mod direwolf;
|
||||||
mod diseased;
|
mod diseased;
|
||||||
mod elder;
|
mod elder;
|
||||||
mod empath;
|
mod empath;
|
||||||
|
|
@ -7,8 +24,11 @@ mod guardian;
|
||||||
mod hunter;
|
mod hunter;
|
||||||
mod insomniac;
|
mod insomniac;
|
||||||
mod lone_wolf;
|
mod lone_wolf;
|
||||||
|
mod maple_wolf;
|
||||||
mod mason;
|
mod mason;
|
||||||
|
mod militia;
|
||||||
mod mortician;
|
mod mortician;
|
||||||
|
mod protector;
|
||||||
mod pyremaster;
|
mod pyremaster;
|
||||||
mod scapegoat;
|
mod scapegoat;
|
||||||
mod shapeshifter;
|
mod shapeshifter;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
game_test::{ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::ActionPromptTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_protect_same_target() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().protector();
|
||||||
|
let prot = game.living_villager();
|
||||||
|
game.mark(prot.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(prot.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protector)
|
||||||
|
.protector_mut()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
Some(prot.character_id())
|
||||||
|
);
|
||||||
|
|
||||||
|
match game.execute() {
|
||||||
|
ActionPrompt::Protector { targets, .. } => {
|
||||||
|
assert!(
|
||||||
|
!targets
|
||||||
|
.into_iter()
|
||||||
|
.any(|c| c.character_id == prot.character_id())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prompt => panic!("expected protector prompt, got {:?}", prompt.title()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_self_protect() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().protector();
|
||||||
|
game.mark(game.character_by_player_id(protector).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(protector).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protector).died_to().cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZero;
|
use core::num::NonZero;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -86,10 +100,6 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
game.mark_and_check(seer);
|
game.mark_and_check(seer);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
|
||||||
game.mark_and_check(wolf_char_id);
|
|
||||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
|
||||||
game.r#continue().sleep();
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -115,20 +125,18 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
.character_id();
|
.character_id();
|
||||||
game.mark_and_check(wolf_target_2);
|
game.mark_and_check(wolf_target_2);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
let scapegoat = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.find(|c| c.player_id() == scapegoat_player_id)
|
|
||||||
.unwrap()
|
|
||||||
.clone();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.next(),
|
game.next(),
|
||||||
ActionPrompt::RoleChange {
|
ActionPrompt::RoleChange {
|
||||||
character_id: scapegoat.identity(),
|
character_id: game.character_by_player_id(scapegoat_player_id).identity(),
|
||||||
new_role: RoleTitle::Seer
|
new_role: RoleTitle::Seer
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(wolf_player_id).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
|
@ -116,7 +130,9 @@ fn protect_stops_shapeshift() {
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
||||||
|
|
||||||
game.response(ActionResponse::Shapeshift);
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
|
|
@ -204,3 +220,60 @@ fn i_would_simply_refuse() {
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shapeshift_fail_can_continue() {
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let shapeshifter = player_ids.next().unwrap();
|
||||||
|
let direwolf = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||||
|
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
let dw_target = game.living_villager();
|
||||||
|
game.mark(dw_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().protector();
|
||||||
|
let ss_target = game.living_villager();
|
||||||
|
game.mark(ss_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(ss_target.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
match game
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Shapeshift,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::ActionResult(_, ActionResult::ShiftFailed) => {}
|
||||||
|
other => panic!("expected shift fail, got {other:?}"),
|
||||||
|
};
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(dw_target.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
use core::num::NonZeroU8;
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
game::{Game, GameOver, GameSettings, SetupRole},
|
||||||
game::{Game, GameOver, GameSettings, OrRandom, SetupRole},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
game_test::{
|
message::night::ActionPromptTitle,
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
|
||||||
SettingsExt, gen_players,
|
|
||||||
},
|
|
||||||
message::{
|
|
||||||
host::{HostDayMessage, HostGameMessage, ServerToHostMessage},
|
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
|
||||||
},
|
|
||||||
role::Role,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
crate::id_impl!(GameId);
|
|
||||||
|
|
@ -1,109 +1,28 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
#[cfg(feature = "server")]
|
pub mod aura;
|
||||||
pub mod cbor;
|
pub mod bag;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod diedto;
|
pub mod diedto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod game_test;
|
mod game_test;
|
||||||
pub mod id;
|
|
||||||
pub mod limited;
|
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod modifier;
|
|
||||||
pub mod nonzero;
|
pub mod nonzero;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod token;
|
pub mod team;
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! id_impl {
|
|
||||||
($name:ident) => {
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct $name(uuid::Uuid);
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl sqlx::TypeInfo for $name {
|
|
||||||
fn is_null(&self) -> bool {
|
|
||||||
self.0 == uuid::Uuid::nil()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl sqlx::Type<sqlx::Postgres> for $name {
|
|
||||||
fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
|
|
||||||
<uuid::Uuid as sqlx::Type<sqlx::Postgres>>::type_info()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
|
|
||||||
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
|
||||||
self.0.encode_by_ref(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name {
|
|
||||||
fn decode(
|
|
||||||
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
|
|
||||||
) -> Result<Self, sqlx::error::BoxDynError> {
|
|
||||||
Ok(Self(uuid::Uuid::decode(value)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<uuid::Uuid> for $name {
|
|
||||||
fn from(value: uuid::Uuid) -> Self {
|
|
||||||
Self::from_uuid(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<$name> for uuid::Uuid {
|
|
||||||
fn from(value: $name) -> Self {
|
|
||||||
value.into_uuid()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for $name {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $name {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(uuid::Uuid::new_v4())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn from_uuid(uuid: uuid::Uuid) -> Self {
|
|
||||||
Self(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn into_uuid(self) -> uuid::Uuid {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Display for $name {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::str::FromStr for $name {
|
|
||||||
type Err = uuid::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Self(uuid::Uuid::from_str(s)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
use core::{
|
|
||||||
fmt::Display,
|
|
||||||
ops::{Deref, RangeInclusive},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct FixedLenString<const LEN: usize>(String);
|
|
||||||
|
|
||||||
impl<const LEN: usize> Display for FixedLenString<LEN> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const LEN: usize> FixedLenString<LEN> {
|
|
||||||
pub fn new(s: String) -> Option<Self> {
|
|
||||||
(s.chars().take(LEN + 1).count() == LEN).then_some(Self(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const LEN: usize> Deref for FixedLenString<LEN> {
|
|
||||||
type Target = String;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, const LEN: usize> Deserialize<'de> for FixedLenString<LEN> {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct ExpectedLen(usize);
|
|
||||||
impl serde::de::Expected for ExpectedLen {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "a string exactly {} characters long", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<String as Deserialize>::deserialize(deserializer).and_then(|s| {
|
|
||||||
let char_count = s.chars().take(LEN.saturating_add(1)).count();
|
|
||||||
if char_count != LEN {
|
|
||||||
Err(serde::de::Error::invalid_length(
|
|
||||||
char_count,
|
|
||||||
&ExpectedLen(LEN),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(Self(s))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const LEN: usize> Serialize for FixedLenString<LEN> {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(self.0.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct ClampedString<const MIN: usize, const MAX: usize>(String);
|
|
||||||
|
|
||||||
impl<const MIN: usize, const MAX: usize> ClampedString<MIN, MAX> {
|
|
||||||
pub fn new(s: String) -> Result<Self, RangeInclusive<usize>> {
|
|
||||||
let str_len = s.chars().take(MAX.saturating_add(1)).count();
|
|
||||||
(str_len >= MIN && str_len <= MAX)
|
|
||||||
.then_some(Self(s))
|
|
||||||
.ok_or(MIN..=MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_inner(self) -> String {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const MIN: usize, const MAX: usize> Display for ClampedString<MIN, MAX> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const MIN: usize, const MAX: usize> Deref for ClampedString<MIN, MAX> {
|
|
||||||
type Target = String;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, const MIN: usize, const MAX: usize> Deserialize<'de> for ClampedString<MIN, MAX> {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct ExpectedLen(usize, usize);
|
|
||||||
impl serde::de::Expected for ExpectedLen {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"a string between {} and {} characters long",
|
|
||||||
self.0, self.1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<String as Deserialize>::deserialize(deserializer).and_then(|s| {
|
|
||||||
let char_count = s.chars().take(MAX.saturating_add(1)).count();
|
|
||||||
if char_count < MIN || char_count > MAX {
|
|
||||||
Err(serde::de::Error::invalid_length(
|
|
||||||
char_count,
|
|
||||||
&ExpectedLen(MIN, MAX),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(Self(s))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const MIN: usize, const MAX: usize> Serialize for ClampedString<MIN, MAX> {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(self.0.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
pub mod host;
|
pub mod host;
|
||||||
mod ident;
|
mod ident;
|
||||||
pub mod night;
|
pub mod night;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{fmt::Display, num::NonZeroU8};
|
use core::{fmt::Display, num::NonZeroU8};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{num::NonZeroU8, ops::Deref};
|
use core::{num::NonZeroU8, ops::Deref};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::{ChecksAs, Titles};
|
use werewolves_macros::{ChecksAs, Extract, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
|
|
@ -13,39 +27,36 @@ use crate::{
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, ChecksAs)]
|
||||||
pub enum ActionType {
|
pub enum ActionType {
|
||||||
Cover,
|
Cover,
|
||||||
|
#[checks("is_wolfy")]
|
||||||
WolvesIntro,
|
WolvesIntro,
|
||||||
|
TraitorIntro,
|
||||||
|
RoleChange,
|
||||||
Protect,
|
Protect,
|
||||||
|
#[checks("is_wolfy")]
|
||||||
WolfPackKill,
|
WolfPackKill,
|
||||||
Direwolf,
|
#[checks("is_wolfy")]
|
||||||
|
Shapeshifter,
|
||||||
|
#[checks("is_wolfy")]
|
||||||
|
AlphaWolfKill,
|
||||||
|
#[checks("is_wolfy")]
|
||||||
OtherWolf,
|
OtherWolf,
|
||||||
|
#[checks("is_wolfy")]
|
||||||
|
Direwolf,
|
||||||
LoneWolfKill,
|
LoneWolfKill,
|
||||||
Block,
|
Block,
|
||||||
|
VillageKill,
|
||||||
Intel,
|
Intel,
|
||||||
Other,
|
Other,
|
||||||
MasonRecruit,
|
MasonRecruit,
|
||||||
MasonsWake,
|
MasonsWake,
|
||||||
Insomniac,
|
Insomniac,
|
||||||
Beholder,
|
Beholder,
|
||||||
RoleChange,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionType {
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)]
|
||||||
const fn is_wolfy(&self) -> bool {
|
|
||||||
// note: Lone Wolf isn't wolfy, as they don't wake with wolves
|
|
||||||
matches!(
|
|
||||||
self,
|
|
||||||
ActionType::Direwolf
|
|
||||||
| ActionType::OtherWolf
|
|
||||||
| ActionType::WolfPackKill
|
|
||||||
| ActionType::WolvesIntro
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)]
|
|
||||||
pub enum ActionPrompt {
|
pub enum ActionPrompt {
|
||||||
#[checks(ActionType::Cover)]
|
#[checks(ActionType::Cover)]
|
||||||
CoverOfDarkness,
|
CoverOfDarkness,
|
||||||
|
|
@ -91,7 +102,7 @@ pub enum ActionPrompt {
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::VillageKill)]
|
||||||
Militia {
|
Militia {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -159,7 +170,7 @@ pub enum ActionPrompt {
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::VillageKill)]
|
||||||
PyreMaster {
|
PyreMaster {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -171,9 +182,9 @@ pub enum ActionPrompt {
|
||||||
living_villagers: Box<[CharacterIdentity]>,
|
living_villagers: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
#[checks(ActionType::OtherWolf)]
|
#[checks(ActionType::Shapeshifter)]
|
||||||
Shapeshifter { character_id: CharacterIdentity },
|
Shapeshifter { character_id: CharacterIdentity },
|
||||||
#[checks(ActionType::OtherWolf)]
|
#[checks(ActionType::AlphaWolfKill)]
|
||||||
AlphaWolf {
|
AlphaWolf {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
living_villagers: Box<[CharacterIdentity]>,
|
living_villagers: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -193,12 +204,107 @@ pub enum ActionPrompt {
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Insomniac)]
|
#[checks(ActionType::Insomniac)]
|
||||||
Insomniac { character_id: CharacterIdentity },
|
Insomniac { character_id: CharacterIdentity },
|
||||||
|
#[checks(ActionType::OtherWolf)]
|
||||||
|
Bloodletter {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::TraitorIntro)]
|
||||||
|
TraitorIntro { character_id: CharacterIdentity },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
|
pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||||
|
match self {
|
||||||
|
ActionPrompt::Seer { marked, .. }
|
||||||
|
| ActionPrompt::Protector { marked, .. }
|
||||||
|
| ActionPrompt::Gravedigger { marked, .. }
|
||||||
|
| ActionPrompt::Hunter { marked, .. }
|
||||||
|
| ActionPrompt::Militia { marked, .. }
|
||||||
|
| ActionPrompt::MapleWolf { marked, .. }
|
||||||
|
| ActionPrompt::Guardian { marked, .. }
|
||||||
|
| ActionPrompt::Adjudicator { marked, .. }
|
||||||
|
| ActionPrompt::PowerSeer { marked, .. }
|
||||||
|
| ActionPrompt::Mortician { marked, .. }
|
||||||
|
| ActionPrompt::Beholder { marked, .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { marked, .. }
|
||||||
|
| ActionPrompt::Empath { marked, .. }
|
||||||
|
| ActionPrompt::Vindicator { marked, .. }
|
||||||
|
| ActionPrompt::PyreMaster { marked, .. }
|
||||||
|
| ActionPrompt::WolfPackKill { marked, .. }
|
||||||
|
| ActionPrompt::AlphaWolf { marked, .. }
|
||||||
|
| ActionPrompt::DireWolf { marked, .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { marked, .. }
|
||||||
|
| ActionPrompt::Bloodletter { marked, .. } => match *marked {
|
||||||
|
Some(marked) => Some((marked, None)),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
marked: (None, Some(marked)),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(marked), None),
|
||||||
|
..
|
||||||
|
} => Some((*marked, None)),
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(marked1), Some(marked2)),
|
||||||
|
..
|
||||||
|
} => Some((*marked1, Some(*marked2))),
|
||||||
|
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
marked: (None, None),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
|
| ActionPrompt::TraitorIntro { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) const fn character_id(&self) -> Option<CharacterId> {
|
||||||
|
match self {
|
||||||
|
ActionPrompt::TraitorIntro { character_id }
|
||||||
|
| ActionPrompt::Insomniac { character_id, .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { character_id, .. }
|
||||||
|
| ActionPrompt::ElderReveal { character_id }
|
||||||
|
| ActionPrompt::RoleChange { character_id, .. }
|
||||||
|
| ActionPrompt::Seer { character_id, .. }
|
||||||
|
| ActionPrompt::Protector { character_id, .. }
|
||||||
|
| ActionPrompt::Arcanist { character_id, .. }
|
||||||
|
| ActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
| ActionPrompt::Hunter { character_id, .. }
|
||||||
|
| ActionPrompt::Militia { character_id, .. }
|
||||||
|
| ActionPrompt::MapleWolf { character_id, .. }
|
||||||
|
| ActionPrompt::Guardian { character_id, .. }
|
||||||
|
| ActionPrompt::Shapeshifter { character_id }
|
||||||
|
| ActionPrompt::AlphaWolf { character_id, .. }
|
||||||
|
| ActionPrompt::Adjudicator { character_id, .. }
|
||||||
|
| ActionPrompt::PowerSeer { character_id, .. }
|
||||||
|
| ActionPrompt::Mortician { character_id, .. }
|
||||||
|
| ActionPrompt::Beholder { character_id, .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
|
||||||
|
| ActionPrompt::Empath { character_id, .. }
|
||||||
|
| ActionPrompt::Vindicator { character_id, .. }
|
||||||
|
| ActionPrompt::PyreMaster { character_id, .. }
|
||||||
|
| ActionPrompt::Bloodletter { character_id, .. }
|
||||||
|
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
||||||
|
|
||||||
|
ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::WolfPackKill { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
|
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ActionPrompt::Insomniac { character_id, .. }
|
ActionPrompt::Insomniac { character_id, .. }
|
||||||
|
| ActionPrompt::Bloodletter { character_id, .. }
|
||||||
| ActionPrompt::Seer { character_id, .. }
|
| ActionPrompt::Seer { character_id, .. }
|
||||||
| ActionPrompt::Arcanist { character_id, .. }
|
| ActionPrompt::Arcanist { character_id, .. }
|
||||||
| ActionPrompt::Gravedigger { character_id, .. }
|
| ActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
|
@ -207,7 +313,8 @@ impl ActionPrompt {
|
||||||
| ActionPrompt::Mortician { character_id, .. }
|
| ActionPrompt::Mortician { character_id, .. }
|
||||||
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
||||||
|
|
||||||
ActionPrompt::Beholder { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Beholder { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -228,16 +335,28 @@ impl ActionPrompt {
|
||||||
| ActionPrompt::LoneWolfKill { .. } => false,
|
| ActionPrompt::LoneWolfKill { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn interactive(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Shapeshifter { .. } => true,
|
||||||
|
_ => !matches!(
|
||||||
|
self.with_mark(CharacterId::new()),
|
||||||
|
Err(GameError::RoleDoesntMark)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||||
let mut prompt = self.clone();
|
let mut prompt = self.clone();
|
||||||
match &mut prompt {
|
match &mut prompt {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::ElderReveal { .. }
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
| ActionPrompt::Shapeshifter { .. }
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
| ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark),
|
||||||
|
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
previous,
|
previous,
|
||||||
|
|
@ -302,7 +421,12 @@ impl ActionPrompt {
|
||||||
Ok(prompt)
|
Ok(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
living_players: targets,
|
living_players: targets,
|
||||||
marked,
|
marked,
|
||||||
..
|
..
|
||||||
|
|
@ -429,11 +553,13 @@ pub enum ActionResponse {
|
||||||
MarkTarget(CharacterId),
|
MarkTarget(CharacterId),
|
||||||
Shapeshift,
|
Shapeshift,
|
||||||
Continue,
|
Continue,
|
||||||
|
ContinueToResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
RoleBlocked,
|
RoleBlocked,
|
||||||
|
Drunk,
|
||||||
Seer(Alignment),
|
Seer(Alignment),
|
||||||
PowerSeer { powerful: Powerful },
|
PowerSeer { powerful: Powerful },
|
||||||
Adjudicator { killer: Killer },
|
Adjudicator { killer: Killer },
|
||||||
|
|
@ -442,10 +568,43 @@ pub enum ActionResult {
|
||||||
Mortician(DiedToTitle),
|
Mortician(DiedToTitle),
|
||||||
Insomniac(Visits),
|
Insomniac(Visits),
|
||||||
Empath { scapegoat: bool },
|
Empath { scapegoat: bool },
|
||||||
|
BeholderSawNothing,
|
||||||
|
BeholderSawEverything,
|
||||||
GoBackToSleep,
|
GoBackToSleep,
|
||||||
|
ShiftFailed,
|
||||||
Continue,
|
Continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ActionResult {
|
||||||
|
pub fn insane(&self) -> Option<Self> {
|
||||||
|
Some(match self {
|
||||||
|
ActionResult::Seer(Alignment::Village) => ActionResult::Seer(Alignment::Wolves),
|
||||||
|
ActionResult::Seer(Alignment::Traitor) | ActionResult::Seer(Alignment::Wolves) => {
|
||||||
|
ActionResult::Seer(Alignment::Village)
|
||||||
|
}
|
||||||
|
ActionResult::PowerSeer { powerful } => ActionResult::PowerSeer {
|
||||||
|
powerful: !*powerful,
|
||||||
|
},
|
||||||
|
ActionResult::Adjudicator { killer } => ActionResult::Adjudicator { killer: !*killer },
|
||||||
|
ActionResult::Arcanist(alignment_eq) => ActionResult::Arcanist(!*alignment_eq),
|
||||||
|
ActionResult::Empath { scapegoat } => ActionResult::Empath {
|
||||||
|
scapegoat: !*scapegoat,
|
||||||
|
},
|
||||||
|
ActionResult::BeholderSawNothing => ActionResult::BeholderSawEverything,
|
||||||
|
ActionResult::BeholderSawEverything => ActionResult::BeholderSawNothing,
|
||||||
|
|
||||||
|
ActionResult::ShiftFailed
|
||||||
|
| ActionResult::RoleBlocked
|
||||||
|
| ActionResult::Drunk
|
||||||
|
| ActionResult::GraveDigger(_)
|
||||||
|
| ActionResult::Mortician(_)
|
||||||
|
| ActionResult::Insomniac(_)
|
||||||
|
| ActionResult::GoBackToSleep
|
||||||
|
| ActionResult::Continue => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Visits(Box<[CharacterIdentity]>);
|
pub struct Visits(Box<[CharacterIdentity]>);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum Modifier {
|
|
||||||
Drunk,
|
|
||||||
Insane,
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::fmt::Display;
|
use core::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -17,12 +31,6 @@ impl PlayerId {
|
||||||
pub const fn from_u128(v: u128) -> Self {
|
pub const fn from_u128(v: u128) -> Self {
|
||||||
Self(uuid::Uuid::from_u128(v))
|
Self(uuid::Uuid::from_u128(v))
|
||||||
}
|
}
|
||||||
pub const fn from_uuid(v: uuid::Uuid) -> Self {
|
|
||||||
Self(v)
|
|
||||||
}
|
|
||||||
pub const fn into_uuid(self) -> uuid::Uuid {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PlayerId {
|
impl Display for PlayerId {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{fmt::Display, num::NonZeroU8, ops::Not};
|
use core::{fmt::Display, num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -117,31 +131,37 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Seer,
|
Seer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Arcanist,
|
Arcanist,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Adjudicator,
|
Adjudicator,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
PowerSeer,
|
PowerSeer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Mortician,
|
Mortician,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Beholder,
|
Beholder,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
|
|
@ -183,6 +203,7 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Gravedigger,
|
Gravedigger,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
|
|
@ -219,7 +240,6 @@ pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
|
||||||
Elder {
|
Elder {
|
||||||
knows_on_night: NonZeroU8,
|
knows_on_night: NonZeroU8,
|
||||||
woken_for_reveal: bool,
|
woken_for_reveal: bool,
|
||||||
|
|
@ -228,17 +248,20 @@ pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
Insomniac,
|
Insomniac,
|
||||||
|
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks("killing_wolf")]
|
||||||
Werewolf,
|
Werewolf,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks("killing_wolf")]
|
||||||
AlphaWolf { killed: Option<CharacterId> },
|
AlphaWolf { killed: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
|
|
@ -249,19 +272,27 @@ pub enum Role {
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks("killing_wolf")]
|
||||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
|
#[checks(Alignment::Wolves)]
|
||||||
|
#[checks(Killer::Killer)]
|
||||||
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks("wolf")]
|
||||||
|
Bloodletter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Role {
|
impl Role {
|
||||||
/// [RoleTitle] as shown to the player on role assignment
|
/// [RoleTitle] as shown to the player on role assignment
|
||||||
pub const fn initial_shown_role(&self) -> RoleTitle {
|
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||||
match self {
|
match self {
|
||||||
Role::Apprentice(_) | Role::Elder { .. } | Role::Insomniac => RoleTitle::Villager,
|
Role::Scapegoat { .. } | Role::Apprentice(_) | Role::Elder { .. } | Role::Insomniac => {
|
||||||
|
RoleTitle::Villager
|
||||||
|
}
|
||||||
_ => self.title(),
|
_ => self.title(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +326,7 @@ impl Role {
|
||||||
|
|
||||||
Role::Werewolf => KillingWolfOrder::Werewolf,
|
Role::Werewolf => KillingWolfOrder::Werewolf,
|
||||||
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
||||||
|
Role::Bloodletter => KillingWolfOrder::Bloodletter,
|
||||||
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
||||||
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
||||||
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
||||||
|
|
@ -307,7 +339,7 @@ impl Role {
|
||||||
| Role::Adjudicator
|
| Role::Adjudicator
|
||||||
| Role::DireWolf { .. }
|
| Role::DireWolf { .. }
|
||||||
| Role::Arcanist
|
| Role::Arcanist
|
||||||
| Role::Seer => true,
|
| Role::Seer | Role::Bloodletter => true,
|
||||||
|
|
||||||
Role::Insomniac // has to at least get one good night of sleep, right?
|
Role::Insomniac // has to at least get one good night of sleep, right?
|
||||||
| Role::Beholder
|
| Role::Beholder
|
||||||
|
|
@ -382,6 +414,7 @@ impl Role {
|
||||||
| Role::Militia { targeted: None }
|
| Role::Militia { targeted: None }
|
||||||
| Role::MapleWolf { .. }
|
| Role::MapleWolf { .. }
|
||||||
| Role::Guardian { .. }
|
| Role::Guardian { .. }
|
||||||
|
| Role::Bloodletter
|
||||||
| Role::Seer => true,
|
| Role::Seer => true,
|
||||||
|
|
||||||
Role::Apprentice(title) => village
|
Role::Apprentice(title) => village
|
||||||
|
|
@ -426,6 +459,7 @@ impl RoleTitle {
|
||||||
pub enum Alignment {
|
pub enum Alignment {
|
||||||
Village,
|
Village,
|
||||||
Wolves,
|
Wolves,
|
||||||
|
Traitor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Alignment {
|
impl Alignment {
|
||||||
|
|
@ -443,6 +477,7 @@ impl Display for Alignment {
|
||||||
match self {
|
match self {
|
||||||
Alignment::Village => f.write_str("Village"),
|
Alignment::Village => f.write_str("Village"),
|
||||||
Alignment::Wolves => f.write_str("Wolves"),
|
Alignment::Wolves => f.write_str("Wolves"),
|
||||||
|
Alignment::Traitor => f.write_str("Traitor"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +493,7 @@ pub enum ArcanistCheck {
|
||||||
pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap();
|
pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap();
|
||||||
pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap();
|
pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap();
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum RoleBlock {
|
pub enum RoleBlock {
|
||||||
Direwolf,
|
Direwolf,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::ChecksAs;
|
||||||
|
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ChecksAs)]
|
||||||
|
pub enum Team {
|
||||||
|
Village,
|
||||||
|
#[checks("evil")]
|
||||||
|
Wolves,
|
||||||
|
AnyEvil,
|
||||||
|
}
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{limited::FixedLenString, user::Username};
|
|
||||||
|
|
||||||
pub const TOKEN_LEN: usize = 0x20;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct Token {
|
|
||||||
pub token: FixedLenString<TOKEN_LEN>,
|
|
||||||
pub username: Username,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Token {
|
|
||||||
pub fn login_token(&self) -> TokenLogin {
|
|
||||||
TokenLogin(self.token.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct TokenLogin(pub FixedLenString<TOKEN_LEN>);
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl axum_extra::headers::authorization::Credentials for TokenLogin {
|
|
||||||
const SCHEME: &'static str = "Bearer";
|
|
||||||
|
|
||||||
fn decode(value: &axum::http::HeaderValue) -> Option<Self> {
|
|
||||||
value
|
|
||||||
.to_str()
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| FixedLenString::new(v.strip_prefix("Bearer ").unwrap_or(v).to_string()))
|
|
||||||
.map(Self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode(&self) -> axum::http::HeaderValue {
|
|
||||||
axum::http::HeaderValue::from_str(self.0.as_str()).expect("bearer token encode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::limited::ClampedString;
|
|
||||||
|
|
||||||
pub type Username = ClampedString<1, 0x40>;
|
|
||||||
pub type Password = ClampedString<6, 0x100>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
|
||||||
pub struct UserLogin {
|
|
||||||
pub username: Username,
|
|
||||||
pub password: Password,
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::id_impl!(UserId);
|
|
||||||
|
|
@ -11,12 +11,12 @@ pretty_env_logger = { version = "0.5" }
|
||||||
# env_logger = { version = "0.11" }
|
# env_logger = { version = "0.11" }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
anyhow = { version = "1" }
|
anyhow = { version = "1" }
|
||||||
werewolves-proto = { path = "../werewolves-proto", features = ["server"] }
|
werewolves-proto = { path = "../werewolves-proto" }
|
||||||
werewolves-macros = { path = "../werewolves-macros" }
|
werewolves-macros = { path = "../werewolves-macros" }
|
||||||
mime-sniffer = { version = "0.1" }
|
mime-sniffer = { version = "0.1" }
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
atom_syndication = { version = "0.12" }
|
atom_syndication = { version = "0.12" }
|
||||||
axum-extra = { version = "0.12", features = ["typed-header"] }
|
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||||
rand = { version = "0.9" }
|
rand = { version = "0.9" }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
@ -24,27 +24,8 @@ thiserror = { version = "2" }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
colored = { version = "3.0" }
|
colored = { version = "3.0" }
|
||||||
fast_qr = { version = "0.13", features = ["svg"] }
|
fast_qr = { version = "0.13", features = ["svg"] }
|
||||||
ron = "0.11"
|
ron = "0.8"
|
||||||
bytes = { version = "1.10" }
|
bytes = { version = "1.10" }
|
||||||
sqlx = { version = "0.8", features = [
|
|
||||||
"runtime-tokio",
|
|
||||||
"postgres",
|
|
||||||
"derive",
|
|
||||||
"macros",
|
|
||||||
"uuid",
|
|
||||||
"chrono",
|
|
||||||
] }
|
|
||||||
argon2 = { version = "0.5" }
|
|
||||||
tower-http = { version = "0.6", features = ["cors"] }
|
|
||||||
tower = { version = "0.5.2", features = [
|
|
||||||
"limit",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tokio-util",
|
|
||||||
"tracing",
|
|
||||||
"buffer",
|
|
||||||
"timeout",
|
|
||||||
] }
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{net::SocketAddr, time::Duration};
|
use core::{net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, LogError, XForwardedFor,
|
AppState, XForwardedFor,
|
||||||
connection::{ConnectionId, JoinedPlayer},
|
connection::{ConnectionId, JoinedPlayer},
|
||||||
runner::IdentifiedClientMessage,
|
runner::IdentifiedClientMessage,
|
||||||
};
|
};
|
||||||
|
|
@ -12,7 +26,7 @@ use axum::{
|
||||||
},
|
},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, headers};
|
use axum_extra::TypedHeader;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
|
|
@ -20,7 +34,6 @@ use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, Up
|
||||||
|
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
|
||||||
x_forwarded_for: Option<TypedHeader<XForwardedFor>>,
|
x_forwarded_for: Option<TypedHeader<XForwardedFor>>,
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -29,12 +42,7 @@ pub async fn handler(
|
||||||
.map(|x| x.to_string())
|
.map(|x| x.to_string())
|
||||||
.unwrap_or_else(|| addr.to_string())
|
.unwrap_or_else(|| addr.to_string())
|
||||||
.italic();
|
.italic();
|
||||||
// log::debug!(
|
|
||||||
// "{who}{} connected.",
|
|
||||||
// user_agent
|
|
||||||
// .map(|agent| format!(" (User-Agent: {})", agent.as_str()))
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
// );
|
|
||||||
let player_list = state.joined_players;
|
let player_list = state.joined_players;
|
||||||
|
|
||||||
// finalize the upgrade process by returning upgrade callback.
|
// finalize the upgrade process by returning upgrade callback.
|
||||||
|
|
@ -194,7 +202,7 @@ impl Client {
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Message::Close(Some(close_frame)) => {
|
Message::Close(Some(_)) => {
|
||||||
// log::debug!("sent close frame: {close_frame:?}");
|
// log::debug!("sent close frame: {close_frame:?}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::sync::atomic::{AtomicBool, Ordering};
|
use core::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use colored::Colorize;
|
||||||
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -31,6 +46,10 @@ impl HostComms {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
||||||
|
log::debug!(
|
||||||
|
"sending message to host: {}",
|
||||||
|
format!("{message:?}").dimmed()
|
||||||
|
);
|
||||||
self.send
|
self.send
|
||||||
.send(message)
|
.send(message)
|
||||||
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use werewolves_proto::error::GameError;
|
use werewolves_proto::error::GameError;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use werewolves_proto::error::GameError;
|
use werewolves_proto::error::GameError;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use tokio::sync::broadcast::Receiver;
|
use tokio::sync::broadcast::Receiver;
|
||||||
use werewolves_proto::error::GameError;
|
use werewolves_proto::error::GameError;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
use sqlx::{Pool, Postgres, query};
|
|
||||||
use werewolves_proto::{
|
|
||||||
error::DatabaseError,
|
|
||||||
game::{Game, GameOver, story::GameStory},
|
|
||||||
id::GameId,
|
|
||||||
player::PlayerId,
|
|
||||||
user::UserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GameDatabase {
|
|
||||||
pub(super) pool: Pool<Postgres>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameDatabase {
|
|
||||||
pub async fn new_game(&self, game: &Game) -> Result<(), DatabaseError> {
|
|
||||||
let state = serde_json::to_value(game.game_state())?;
|
|
||||||
let story = serde_json::to_value(&game.story())?;
|
|
||||||
|
|
||||||
let mut tx = self.pool.begin().await?;
|
|
||||||
query!(
|
|
||||||
r#" insert into
|
|
||||||
games (id, started_at, state, story)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4)"#,
|
|
||||||
game.game_id().into_uuid(),
|
|
||||||
game.started_at(),
|
|
||||||
state,
|
|
||||||
story,
|
|
||||||
)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let player_ids = game
|
|
||||||
.village()
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| c.player_id().into_uuid())
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
let user_ids = query!(
|
|
||||||
r#" select
|
|
||||||
id, user_id
|
|
||||||
from
|
|
||||||
players
|
|
||||||
where
|
|
||||||
id = any($1::uuid[])"#,
|
|
||||||
&*player_ids,
|
|
||||||
)
|
|
||||||
.fetch_all(&mut *tx)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| (PlayerId::from_uuid(r.id), r.user_id.map(UserId::from_uuid)))
|
|
||||||
.unzip::<PlayerId, Option<UserId>, Vec<PlayerId>, Vec<Option<UserId>>>();
|
|
||||||
|
|
||||||
let game_id = game.game_id().into_uuid();
|
|
||||||
let game_ids = (0..player_ids.len()).map(|_| game_id).collect::<Box<[_]>>();
|
|
||||||
|
|
||||||
query!(
|
|
||||||
r#" with
|
|
||||||
game_ids as (select row_number() over(), * from unnest($1::uuid[]) as game_id),
|
|
||||||
player_ids as (select row_number() over(), * from unnest($2::uuid[]) as player_id)
|
|
||||||
insert into
|
|
||||||
game_players
|
|
||||||
select
|
|
||||||
game_ids.game_id, player_ids.player_id
|
|
||||||
from
|
|
||||||
game_ids
|
|
||||||
join
|
|
||||||
player_ids on game_ids.row_number = player_ids.row_number
|
|
||||||
"#,
|
|
||||||
&*game_ids,
|
|
||||||
&*player_ids,
|
|
||||||
)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_game(&self, game: &Game) -> Result<(), DatabaseError> {
|
|
||||||
let state = serde_json::to_value(game.game_state())?;
|
|
||||||
let story = serde_json::to_value(game.story())?;
|
|
||||||
|
|
||||||
query!(
|
|
||||||
r#" update
|
|
||||||
games
|
|
||||||
set
|
|
||||||
story = $2,
|
|
||||||
state = $3,
|
|
||||||
outcome = $4,
|
|
||||||
updated_at = now()
|
|
||||||
where
|
|
||||||
id = $1"#,
|
|
||||||
game.game_id().into_uuid(),
|
|
||||||
story,
|
|
||||||
state,
|
|
||||||
game.game_over().map(Self::outcome_to_db_outcome) as _
|
|
||||||
)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_active_game(&self) -> Result<Game, DatabaseError> {
|
|
||||||
let game = query!(
|
|
||||||
r#" select
|
|
||||||
id, state, story, started_at
|
|
||||||
from
|
|
||||||
games
|
|
||||||
where
|
|
||||||
outcome is null"#
|
|
||||||
)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Game::new_from_parts(
|
|
||||||
GameId::from_uuid(game.id),
|
|
||||||
game.started_at,
|
|
||||||
serde_json::from_value(game.story)?,
|
|
||||||
serde_json::from_value(game.state)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_game_story(&self, id: GameId) -> Result<GameStory, DatabaseError> {
|
|
||||||
let game = query!(
|
|
||||||
r#" select
|
|
||||||
story
|
|
||||||
from
|
|
||||||
games
|
|
||||||
where
|
|
||||||
id = $1
|
|
||||||
and
|
|
||||||
outcome is not null"#,
|
|
||||||
id.into_uuid(),
|
|
||||||
)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(serde_json::from_value(game.story)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn outcome_to_db_outcome(outcome: GameOver) -> &'static str {
|
|
||||||
match outcome {
|
|
||||||
GameOver::VillageWins => "village_victory",
|
|
||||||
GameOver::WolvesWin => "wolves_victory",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
pub mod game;
|
|
||||||
pub mod user;
|
|
||||||
use sqlx::{Pool, Postgres};
|
|
||||||
use werewolves_proto::error::DatabaseError;
|
|
||||||
|
|
||||||
use crate::db::{game::GameDatabase, user::UserDatabase};
|
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, DatabaseError>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Database {
|
|
||||||
pool: Pool<Postgres>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub const fn new(pool: Pool<Postgres>) -> Self {
|
|
||||||
Self { pool }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user(&self) -> UserDatabase {
|
|
||||||
UserDatabase {
|
|
||||||
pool: self.pool.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn game(&self) -> GameDatabase {
|
|
||||||
GameDatabase {
|
|
||||||
pool: self.pool.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn migrate(&self) {
|
|
||||||
log::info!("running migrations");
|
|
||||||
sqlx::migrate!("../migrations")
|
|
||||||
.run(&self.pool)
|
|
||||||
.await
|
|
||||||
.expect("run migrations");
|
|
||||||
log::info!("migrations done");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
use super::Result;
|
|
||||||
use argon2::{
|
|
||||||
Argon2, PasswordHash, PasswordVerifier,
|
|
||||||
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
|
||||||
};
|
|
||||||
use chrono::{TimeDelta, Utc};
|
|
||||||
|
|
||||||
use rand::distr::SampleString;
|
|
||||||
use sqlx::{Decode, Encode, Pool, Postgres, prelude::FromRow, query, query_as};
|
|
||||||
use werewolves_proto::{
|
|
||||||
error::{DatabaseError, ServerError},
|
|
||||||
token,
|
|
||||||
user::UserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UserDatabase {
|
|
||||||
pub(super) pool: Pool<Postgres>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow)]
|
|
||||||
pub struct LoginToken {
|
|
||||||
pub token: String,
|
|
||||||
pub user_id: UserId,
|
|
||||||
|
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
|
||||||
pub expires_at: chrono::DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginToken {
|
|
||||||
const TOKEN_LONGEVITY: TimeDelta = TimeDelta::days(30);
|
|
||||||
|
|
||||||
pub fn new(user_id: UserId) -> Self {
|
|
||||||
let created_at = Utc::now();
|
|
||||||
let expires_at = created_at
|
|
||||||
.checked_add_signed(Self::TOKEN_LONGEVITY)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
panic!(
|
|
||||||
"could not add {} time to {created_at}",
|
|
||||||
Self::TOKEN_LONGEVITY
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let token = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), token::TOKEN_LEN);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
token,
|
|
||||||
user_id,
|
|
||||||
created_at,
|
|
||||||
expires_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum GetUserBy<'a> {
|
|
||||||
Username(&'a str),
|
|
||||||
Id(UserId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserDatabase {
|
|
||||||
pub async fn create(&self, username: &str, password: &str) -> Result<User> {
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
|
||||||
let argon2 = Argon2::default();
|
|
||||||
let password_hash = argon2
|
|
||||||
.hash_password(password.as_bytes(), &salt)?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let now = chrono::offset::Utc::now();
|
|
||||||
|
|
||||||
let user = User {
|
|
||||||
id: UserId::new(),
|
|
||||||
username: username.into(),
|
|
||||||
password_hash,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
query!(
|
|
||||||
r#"insert into users
|
|
||||||
(id, username, password_hash, created_at, updated_at)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4, $5)"#,
|
|
||||||
user.id.into_uuid(),
|
|
||||||
user.username,
|
|
||||||
user.password_hash,
|
|
||||||
user.created_at,
|
|
||||||
user.updated_at
|
|
||||||
)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
if let sqlx::Error::Database(db_err) = &err
|
|
||||||
&& let Some(constraint) = db_err.constraint()
|
|
||||||
&& constraint == "users_username_unique"
|
|
||||||
{
|
|
||||||
DatabaseError::UserAlreadyExists
|
|
||||||
} else {
|
|
||||||
err.into()
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_user(&self, get_user_by: GetUserBy<'_>) -> Result<User> {
|
|
||||||
Ok(match get_user_by {
|
|
||||||
GetUserBy::Username(username) => {
|
|
||||||
query_as!(
|
|
||||||
User,
|
|
||||||
r#"
|
|
||||||
select
|
|
||||||
id, username, password_hash,
|
|
||||||
created_at, updated_at
|
|
||||||
from
|
|
||||||
users
|
|
||||||
where
|
|
||||||
username = $1"#,
|
|
||||||
username
|
|
||||||
)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
GetUserBy::Id(id) => {
|
|
||||||
query_as!(
|
|
||||||
User,
|
|
||||||
r#"
|
|
||||||
select
|
|
||||||
id, username, password_hash,
|
|
||||||
created_at, updated_at
|
|
||||||
from
|
|
||||||
users
|
|
||||||
where
|
|
||||||
id = $1"#,
|
|
||||||
id.into_uuid()
|
|
||||||
)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(
|
|
||||||
&self,
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> core::result::Result<LoginToken, ServerError> {
|
|
||||||
let user = self.get_user(GetUserBy::Username(username)).await?;
|
|
||||||
|
|
||||||
let parsed_hash = PasswordHash::new(&user.password_hash).map_err(DatabaseError::from)?;
|
|
||||||
Argon2::default()
|
|
||||||
.verify_password(password.as_bytes(), &parsed_hash)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
argon2::password_hash::Error::Password => ServerError::InvalidCredentials,
|
|
||||||
err => ServerError::DatabaseError(err.into()),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let token = LoginToken::new(user.id);
|
|
||||||
|
|
||||||
query!(
|
|
||||||
r#" insert into login_tokens
|
|
||||||
(token, user_id, created_at, expires_at)
|
|
||||||
values
|
|
||||||
($1, $2, $3, $4)"#,
|
|
||||||
token.token,
|
|
||||||
token.user_id.into_uuid(),
|
|
||||||
token.created_at,
|
|
||||||
token.expires_at
|
|
||||||
)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Into::<DatabaseError>::into)?;
|
|
||||||
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_token(&self, token: &str) -> core::result::Result<User, ServerError> {
|
|
||||||
let token = query_as!(
|
|
||||||
LoginToken,
|
|
||||||
r#" select
|
|
||||||
token, user_id, created_at, expires_at
|
|
||||||
from
|
|
||||||
login_tokens
|
|
||||||
where
|
|
||||||
token = $1
|
|
||||||
and
|
|
||||||
expires_at > now()
|
|
||||||
"#,
|
|
||||||
token
|
|
||||||
)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Into::<DatabaseError>::into)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
DatabaseError::NotFound => ServerError::ExpiredToken,
|
|
||||||
_ => err.into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if Utc::now() >= token.expires_at {
|
|
||||||
return Err(ServerError::ExpiredToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.get_user(GetUserBy::Id(token.user_id)).await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow, Encode, Decode)]
|
|
||||||
pub struct User {
|
|
||||||
pub id: UserId,
|
|
||||||
pub username: String,
|
|
||||||
pub password_hash: String,
|
|
||||||
|
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
|
||||||
pub updated_at: chrono::DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::ops::Not;
|
use core::ops::Not;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -282,7 +296,7 @@ impl GameRunner {
|
||||||
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
||||||
HostMessage::InGame(msg) => self.game.process(msg),
|
HostMessage::InGame(msg) => self.game.process(msg),
|
||||||
HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => {
|
HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => {
|
||||||
Err(GameError::InvalidMessageForGameState)
|
Err(GameError::GameOngoing)
|
||||||
}
|
}
|
||||||
HostMessage::Echo(echo) => Ok(echo),
|
HostMessage::Echo(echo) => Ok(echo),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::net::SocketAddr;
|
use core::net::SocketAddr;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|
@ -132,7 +146,6 @@ impl Host {
|
||||||
msg = self.server_recv.recv() => {
|
msg = self.server_recv.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
log::debug!("sending message to host: {}", format!("{msg:?}").dimmed());
|
|
||||||
if let Err(err) = self.send_message(&msg).await {
|
if let Err(err) = self.send_message(&msg).await {
|
||||||
log::error!("{} {err}", "[host::outgoing]".bold())
|
log::error!("{} {err}", "[host::outgoing]".bold())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{
|
use core::{
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,51 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
mod client;
|
mod client;
|
||||||
mod communication;
|
mod communication;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod db;
|
|
||||||
mod game;
|
mod game;
|
||||||
mod host;
|
mod host;
|
||||||
mod lobby;
|
mod lobby;
|
||||||
mod runner;
|
mod runner;
|
||||||
// mod saver;
|
mod saver;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
BoxError, Router,
|
Router,
|
||||||
error_handling::HandleErrorLayer,
|
|
||||||
extract::{Path, State},
|
|
||||||
http::{Request, StatusCode, header},
|
http::{Request, StatusCode, header},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{any, get, post, put},
|
routing::{any, get},
|
||||||
};
|
|
||||||
use axum_extra::{
|
|
||||||
TypedHeader,
|
|
||||||
headers::{self, Authorization},
|
|
||||||
};
|
};
|
||||||
|
use axum_extra::headers;
|
||||||
use communication::lobby::LobbyComms;
|
use communication::lobby::LobbyComms;
|
||||||
use connection::JoinedPlayers;
|
use connection::JoinedPlayers;
|
||||||
use core::{fmt::Display, net::SocketAddr, str::FromStr, time::Duration};
|
use core::{fmt::Display, net::SocketAddr, str::FromStr};
|
||||||
use fast_qr::convert::{Builder, Shape, svg::SvgBuilder};
|
use fast_qr::convert::{Builder, Shape, svg::SvgBuilder};
|
||||||
use runner::IdentifiedClientMessage;
|
use runner::IdentifiedClientMessage;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use std::{env, io::Write, path::Path};
|
||||||
use std::{env, io::Write};
|
|
||||||
use tokio::sync::{broadcast, mpsc};
|
use tokio::sync::{broadcast, mpsc};
|
||||||
use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer};
|
|
||||||
use werewolves_proto::{
|
|
||||||
cbor::Cbor,
|
|
||||||
error::ServerError,
|
|
||||||
id::GameId,
|
|
||||||
limited::FixedLenString,
|
|
||||||
token::{Token, TokenLogin},
|
|
||||||
user::UserLogin,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
communication::{Comms, connect::ConnectUpdate, host::HostComms, player::PlayerIdComms},
|
communication::{Comms, connect::ConnectUpdate, host::HostComms, player::PlayerIdComms},
|
||||||
db::Database,
|
saver::FileSaver,
|
||||||
// saver::FileSaver,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 8080;
|
const DEFAULT_PORT: u16 = 8080;
|
||||||
const DEFAULT_HOST: &str = "127.0.0.1";
|
const DEFAULT_HOST: &str = "127.0.0.1";
|
||||||
|
const DEFAULT_SAVE_DIR: &str = "werewolves-saves/";
|
||||||
const DEFAULT_QRCODE_URL: &str = "https://wolf.emilis.dev/";
|
const DEFAULT_QRCODE_URL: &str = "https://wolf.emilis.dev/";
|
||||||
|
|
||||||
const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30;
|
|
||||||
const DEFAULT_PG_CONN_STRING: &str = "postgres:///ww?host=/var/run/postgresql";
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// pretty_env_logger::init();
|
// pretty_env_logger::init();
|
||||||
|
|
@ -125,60 +120,39 @@ async fn main() {
|
||||||
|
|
||||||
let jp_clone = joined_players.clone();
|
let jp_clone = joined_players.clone();
|
||||||
|
|
||||||
let pg_pool = PgPoolOptions::new()
|
let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR));
|
||||||
.max_connections(
|
|
||||||
std::env::var("MAX_DB_CONNECTIONS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|val| u32::from_str(&val).ok())
|
|
||||||
.unwrap_or(DEFAULT_MAX_PG_CONNECTIONS),
|
|
||||||
)
|
|
||||||
.connect(
|
|
||||||
std::env::var("PG_CONN_STRING")
|
|
||||||
.unwrap_or_else(|_| String::from(DEFAULT_PG_CONN_STRING))
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("could not init db");
|
|
||||||
let db = Database::new(pg_pool);
|
|
||||||
db.migrate().await;
|
|
||||||
|
|
||||||
// let saver = FileSaver::new(path.canonicalize().expect("canonicalizing path"));
|
if let Err(err) = std::fs::create_dir(path)
|
||||||
tokio::spawn({
|
&& !matches!(err.kind(), std::io::ErrorKind::AlreadyExists)
|
||||||
let db = db.clone();
|
{
|
||||||
async move {
|
panic!("creating save dir at [{path:?}]: {err}")
|
||||||
crate::runner::run_game(jp_clone, lobby_comms, db).await;
|
|
||||||
panic!("game over");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we can write to the path
|
||||||
|
{
|
||||||
|
let test_file_path = path.join(".test");
|
||||||
|
if let Err(err) = std::fs::File::create(&test_file_path) {
|
||||||
|
panic!("can't create files in {path:?}: {err}")
|
||||||
|
}
|
||||||
|
std::fs::remove_file(&test_file_path).log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
let saver = FileSaver::new(path.canonicalize().expect("canonicalizing path"));
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::runner::run_game(jp_clone, lobby_comms, saver).await;
|
||||||
|
panic!("game over");
|
||||||
});
|
});
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
joined_players,
|
joined_players,
|
||||||
host_recv,
|
host_recv,
|
||||||
host_send,
|
host_send,
|
||||||
send,
|
send,
|
||||||
db,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/connect/client", any(client::handler))
|
.route("/connect/client", any(client::handler))
|
||||||
.route("/connect/host", any(host::handler))
|
.route("/connect/host", any(host::handler))
|
||||||
.route("/qrcode", get(handle_qr_code))
|
.route("/qrcode", get(handle_qr_code))
|
||||||
.route("/s/users", put(signup))
|
|
||||||
.route("/s/tokens", post(signin))
|
|
||||||
.route(
|
|
||||||
"/s/tokens/check",
|
|
||||||
get(check_token).layer(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(HandleErrorLayer::new(|err: BoxError| async move {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Unhandled error: {}", err),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.layer(BufferLayer::new(0x100))
|
|
||||||
.layer(RateLimitLayer::new(100, Duration::from_secs(10))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route("/s/games/{id}", get(get_game_by_id))
|
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.fallback(get(handle_http_static));
|
.fallback(get(handle_http_static));
|
||||||
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
||||||
|
|
@ -191,61 +165,15 @@ async fn main() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_game_by_id(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
Path(game_id): Path<GameId>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
let story = db.game().get_game_story(game_id).await?;
|
|
||||||
Ok(Cbor(story))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_token(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
TypedHeader(Authorization(login)): TypedHeader<Authorization<TokenLogin>>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
db.user().check_token(&login.0).await?;
|
|
||||||
Ok(StatusCode::OK)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn signin(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
Cbor(UserLogin { username, password }): Cbor<UserLogin>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
let token = db.user().login(&username, &password).await?;
|
|
||||||
|
|
||||||
Ok(Cbor(Token {
|
|
||||||
username,
|
|
||||||
token: FixedLenString::new(token.token.clone()).ok_or_else(|| {
|
|
||||||
ServerError::InternalServerError(format!(
|
|
||||||
"could not get a fixed len string for token [{}]",
|
|
||||||
token.token
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
created_at: token.created_at,
|
|
||||||
expires_at: token.expires_at,
|
|
||||||
})
|
|
||||||
.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn signup(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
Cbor(UserLogin { username, password }): Cbor<UserLogin>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
db.user().create(&username, &password).await?;
|
|
||||||
Ok(StatusCode::CREATED)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
joined_players: JoinedPlayers,
|
joined_players: JoinedPlayers,
|
||||||
send: broadcast::Sender<IdentifiedClientMessage>,
|
send: broadcast::Sender<IdentifiedClientMessage>,
|
||||||
host_send: tokio::sync::mpsc::Sender<werewolves_proto::message::host::HostMessage>,
|
host_send: tokio::sync::mpsc::Sender<werewolves_proto::message::host::HostMessage>,
|
||||||
host_recv: broadcast::Receiver<werewolves_proto::message::host::ServerToHostMessage>,
|
host_recv: broadcast::Receiver<werewolves_proto::message::host::ServerToHostMessage>,
|
||||||
db: Database,
|
|
||||||
}
|
}
|
||||||
impl Clone for AppState {
|
impl Clone for AppState {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db: self.db.clone(),
|
|
||||||
joined_players: self.joined_players.clone(),
|
joined_players: self.joined_players.clone(),
|
||||||
send: self.send.clone(),
|
send: self.send.clone(),
|
||||||
host_send: self.host_send.clone(),
|
host_send: self.host_send.clone(),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{num::NonZeroU8, time::Duration};
|
use core::{num::NonZeroU8, time::Duration};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::{GameError, ServerError},
|
message::{ClientMessage, Identification, host::HostMessage},
|
||||||
message::{
|
|
||||||
ClientMessage, Identification,
|
|
||||||
host::{HostMessage, ServerToHostMessage},
|
|
||||||
},
|
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -14,9 +24,9 @@ use crate::{
|
||||||
LogError,
|
LogError,
|
||||||
communication::lobby::LobbyComms,
|
communication::lobby::LobbyComms,
|
||||||
connection::JoinedPlayers,
|
connection::JoinedPlayers,
|
||||||
db::Database,
|
|
||||||
game::{GameEnd, GameRunner},
|
game::{GameEnd, GameRunner},
|
||||||
lobby::{Lobby, LobbyPlayers},
|
lobby::{Lobby, LobbyPlayers},
|
||||||
|
saver::Saver,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
@ -25,7 +35,7 @@ pub struct IdentifiedClientMessage {
|
||||||
pub message: ClientMessage,
|
pub message: ClientMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, db: Database) {
|
pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, mut saver: impl Saver) {
|
||||||
let mut lobby = Lobby::new(joined_players, comms);
|
let mut lobby = Lobby::new(joined_players, comms);
|
||||||
if let Some(dummies) = option_env!("DUMMY_PLAYERS").and_then(|p| p.parse::<NonZeroU8>().ok()) {
|
if let Some(dummies) = option_env!("DUMMY_PLAYERS").and_then(|p| p.parse::<NonZeroU8>().ok()) {
|
||||||
log::info!("creating {dummies} dummy players");
|
log::info!("creating {dummies} dummy players");
|
||||||
|
|
@ -36,48 +46,43 @@ pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, db: Data
|
||||||
loop {
|
loop {
|
||||||
match &mut state {
|
match &mut state {
|
||||||
RunningState::Lobby(lobby) => {
|
RunningState::Lobby(lobby) => {
|
||||||
if let Some(mut game) = lobby.next().await {
|
if let Some(game) = lobby.next().await {
|
||||||
if let Err(err) = db.game().new_game(game.proto_game()).await {
|
|
||||||
log::error!("saving new game: {err}; reverting to lobby");
|
|
||||||
game.comms()
|
|
||||||
.host()
|
|
||||||
.send(ServerToHostMessage::Error(GameError::ServerError(
|
|
||||||
ServerError::DatabaseError(err),
|
|
||||||
)))
|
|
||||||
.log_err();
|
|
||||||
|
|
||||||
state = RunningState::Lobby(game.into_lobby());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state = RunningState::Game(game)
|
state = RunningState::Game(game)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RunningState::Game(game) => {
|
RunningState::Game(game) => {
|
||||||
if let Some(result) = game.next().await {
|
if let Some(result) = game.next().await {
|
||||||
if let Err(err) = db.game().update_game(game.proto_game()).await {
|
match saver.save(game.proto_game()) {
|
||||||
log::error!("saving game ({}): {err}", game.proto_game().game_id());
|
Ok(path) => {
|
||||||
|
log::info!("saved game to {path}");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("saving game: {err}");
|
||||||
let game_clone = game.proto_game().clone();
|
let game_clone = game.proto_game().clone();
|
||||||
let db_clone = db.game();
|
let mut saver_clone = saver.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let started = chrono::Utc::now();
|
let started = chrono::Utc::now();
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||||
if let Err(err) = db_clone.update_game(&game_clone).await {
|
match saver_clone.save(&game_clone) {
|
||||||
|
Ok(path) => {
|
||||||
|
log::info!("saved game from {started} to {path}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
log::error!("saving game from {started}: {err}")
|
log::error!("saving game from {started}: {err}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
state = match state {
|
state = match state {
|
||||||
RunningState::Game(game) => {
|
RunningState::Game(game) => {
|
||||||
RunningState::GameOver(GameEnd::new(game, result))
|
RunningState::GameOver(GameEnd::new(game, result))
|
||||||
}
|
}
|
||||||
_ => unsafe { core::hint::unreachable_unchecked() },
|
_ => unsafe { core::hint::unreachable_unchecked() },
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
if let Err(err) = db.game().update_game(game.proto_game()).await {
|
|
||||||
log::error!("updating game ({}): {err}", game.proto_game().game_id());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RunningState::GameOver(end) => {
|
RunningState::GameOver(end) => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::fmt::Display;
|
use core::fmt::Display;
|
||||||
use std::{io::Write, path::PathBuf};
|
use std::{io::Write, path::PathBuf};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
[build]
|
[build]
|
||||||
target = "index.html" # The index HTML file to drive the bundling process.
|
target = "index.html" # The index HTML file to drive the bundling process.
|
||||||
html_output = "index.html" # The name of the output HTML file.
|
html_output = "index.html" # The name of the output HTML file.
|
||||||
release = false # Build in release mode.
|
release = true # Build in release mode.
|
||||||
# release = true # Build in release mode.
|
|
||||||
dist = "dist" # The output dir for all final assets.
|
dist = "dist" # The output dir for all final assets.
|
||||||
public_url = "/" # The public URL from which assets are to be served.
|
public_url = "/" # The public URL from which assets are to be served.
|
||||||
filehash = true # Whether to include hash values in the output file names.
|
filehash = true # Whether to include hash values in the output file names.
|
||||||
|
|
@ -10,6 +9,6 @@ inject_scripts = true # Whether to inject scripts (and module preloads) in
|
||||||
offline = false # Run without network access
|
offline = false # Run without network access
|
||||||
frozen = false # Require Cargo.lock and cache are up to date
|
frozen = false # Require Cargo.lock and cache are up to date
|
||||||
locked = false # Require Cargo.lock is up to date
|
locked = false # Require Cargo.lock is up to date
|
||||||
minify = "on_release" # Control minification: can be one of: never, on_release, always
|
# minify = "on_release" # Control minification: can be one of: never, on_release, always
|
||||||
# minify = "always" # Control minification: can be one of: never, on_release, always
|
minify = "always" # Control minification: can be one of: never, on_release, always
|
||||||
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="45.363396mm"
|
width="58.49649mm"
|
||||||
height="45.36343mm"
|
height="55.059494mm"
|
||||||
viewBox="0 0 45.363396 45.36343"
|
viewBox="0 0 58.49649 55.059494"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
|
@ -24,9 +24,9 @@
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:zoom="1"
|
inkscape:zoom="2.8284271"
|
||||||
inkscape:cx="1108.5"
|
inkscape:cx="470.75634"
|
||||||
inkscape:cy="797.49998"
|
inkscape:cy="2967.1969"
|
||||||
inkscape:window-width="1918"
|
inkscape:window-width="1918"
|
||||||
inkscape:window-height="1042"
|
inkscape:window-height="1042"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
inkscape:current-layer="layer4"><inkscape:grid
|
inkscape:current-layer="layer4"><inkscape:grid
|
||||||
id="grid1"
|
id="grid1"
|
||||||
units="mm"
|
units="mm"
|
||||||
originx="-266.17087"
|
originx="-29.360038"
|
||||||
originy="-217.22292"
|
originy="-825.92692"
|
||||||
spacingx="0.26458333"
|
spacingx="0.26458333"
|
||||||
spacingy="0.26458334"
|
spacingy="0.26458334"
|
||||||
empcolor="#0099e5"
|
empcolor="#0099e5"
|
||||||
|
|
@ -48,46 +48,232 @@
|
||||||
visible="false" /><inkscape:page
|
visible="false" /><inkscape:page
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
width="45.363396"
|
width="58.49649"
|
||||||
height="45.36343"
|
height="55.059494"
|
||||||
id="page2"
|
id="page2"
|
||||||
margin="0"
|
margin="0"
|
||||||
bleed="0" /></sodipodi:namedview><defs
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
id="defs1" /><g
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-5"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-5-67"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-8-3"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /></defs><g
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer4"
|
id="layer4"
|
||||||
inkscape:label="Layer 4"
|
inkscape:label="Layer 4"
|
||||||
transform="translate(-266.17087,-217.22291)"><g
|
transform="translate(-29.360037,-825.92694)"><g
|
||||||
id="g61"
|
id="g43"
|
||||||
transform="translate(-56.091665,-3.7041666)"><path
|
transform="translate(0.07389574)"><g
|
||||||
d="m 347.07926,221.66934 c -0.45815,-0.0281 -0.90857,0.004 -1.35031,0.0992 -1.05487,0.22775 -2.05439,0.76275 -2.99671,1.56425 -1.13002,-0.2664 -2.18934,-0.42422 -3.16415,-0.46147 -0.97482,-0.0373 -1.86513,0.0461 -2.65772,0.26096 -0.38599,0.10465 -0.74872,0.24076 -1.08675,0.40928 -0.41079,0.20479 -0.78496,0.45722 -1.11983,0.76068 -0.79968,0.72467 -1.398,1.68777 -1.81333,2.85306 -2.22365,0.66859 -3.98388,1.57251 -5.1418,2.7373 -0.28196,0.28362 -0.52842,0.58265 -0.73691,0.89762 -0.25336,0.38274 -0.45083,0.7888 -0.58911,1.21904 -0.33021,1.02743 -0.36713,2.16074 -0.14418,3.37757 -1.59144,1.69085 -2.66416,3.35361 -3.08456,4.9413 -0.10237,0.38661 -0.16607,0.76866 -0.18914,1.14567 -0.028,0.45815 0.004,0.90857 0.0992,1.35031 0.22775,1.05487 0.76275,2.05439 1.56424,2.99671 -0.5328,2.26004 -0.63027,4.23669 -0.2005,5.82187 0.10465,0.38599 0.24024,0.74872 0.40876,1.08675 0.20479,0.41079 0.45773,0.78496 0.76119,1.11983 0.72468,0.79968 1.68778,1.398 2.85306,1.81333 0.6686,2.22365 1.57252,3.9844 2.7373,5.14232 0.28363,0.28196 0.58266,0.5279 0.89762,0.73639 0.38275,0.25336 0.7888,0.45083 1.21905,0.58911 1.02742,0.33021 2.16074,0.36713 3.37757,0.14418 1.69085,1.59144 3.35361,2.66417 4.9413,3.08456 0.3866,0.10237 0.76866,0.16607 1.14567,0.18914 0.45814,0.028 0.90856,-0.004 1.3503,-0.0992 1.05488,-0.22774 2.05439,-0.76274 2.99672,-1.56424 2.26004,0.5328 4.23668,0.63027 5.82186,0.2005 0.386,-0.10465 0.74873,-0.24024 1.08676,-0.40876 0.41079,-0.20479 0.78496,-0.45773 1.11983,-0.76119 0.79967,-0.72468 1.398,-1.68778 1.81332,-2.85306 2.22366,-0.6686 3.9844,-1.57252 5.14233,-2.7373 0.28195,-0.28363 0.5279,-0.58266 0.73639,-0.89762 0.25335,-0.38275 0.45083,-0.7888 0.58911,-1.21905 0.3302,-1.02742 0.36713,-2.16074 0.14417,-3.37757 1.59144,-1.69085 2.66417,-3.35361 3.08457,-4.9413 0.10237,-0.3866 0.16606,-0.76866 0.18914,-1.14567 0.028,-0.45814 -0.004,-0.90856 -0.0992,-1.3503 -0.22775,-1.05488 -0.76275,-2.05439 -1.56425,-2.99672 0.5328,-2.26003 0.63028,-4.23668 0.2005,-5.82186 -0.10465,-0.386 -0.24024,-0.74873 -0.40876,-1.08676 -0.20479,-0.41079 -0.45773,-0.78495 -0.76119,-1.11983 -0.72467,-0.79967 -1.68777,-1.39799 -2.85306,-1.81332 -0.66859,-2.22366 -1.57251,-3.98389 -2.7373,-5.14181 -0.28362,-0.28195 -0.58266,-0.52842 -0.89762,-0.7369 -0.38274,-0.25336 -0.7888,-0.45084 -1.21904,-0.58912 -1.02743,-0.3302 -2.16075,-0.36713 -3.37757,-0.14417 -1.69085,-1.59144 -3.35361,-2.66417 -4.94131,-3.08457 -0.3866,-0.10237 -0.76866,-0.16606 -1.14566,-0.18913 z m -2.62206,11.10371 c 1.90227,0.35928 3.73774,0.76228 5.48338,1.2082 1.46778,1.26227 2.85564,2.5294 4.14445,3.78839 0.63999,1.82705 1.20834,3.61793 1.69499,5.35265 -0.35928,1.90227 -0.76228,3.73723 -1.2082,5.48287 -1.26227,1.46777 -2.52889,2.85563 -3.78788,4.14445 -1.82705,0.63999 -3.61792,1.20833 -5.35264,1.69499 -1.90227,-0.35928 -3.73775,-0.76229 -5.48339,-1.2082 -1.46777,-1.26228 -2.85563,-2.52889 -4.14445,-3.78788 -0.63999,-1.82705 -1.20833,-3.61792 -1.69499,-5.35265 0.35928,-1.90226 0.76229,-3.73774 1.2082,-5.48338 1.26228,-1.46778 2.52889,-2.85564 3.78788,-4.14445 1.82705,-0.63999 3.61792,-1.20834 5.35265,-1.69499 z"
|
id="g42-4"
|
||||||
style="fill:#d5ffc9;fill-opacity:1;stroke:#5fff32;stroke-width:1.465;stroke-opacity:1"
|
transform="matrix(0.70710678,0.70711411,-0.70710678,0.70711411,584.35218,19.075908)"><ellipse
|
||||||
id="path40-7" /><path
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
sodipodi:type="spiral"
|
id="path30-7-7-7-0"
|
||||||
style="fill:none;fill-rule:evenodd;stroke:#86ff61;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
|
cx="640.62866"
|
||||||
id="path61"
|
cy="431.71231"
|
||||||
sodipodi:cx="342.65289"
|
rx="15.000077"
|
||||||
sodipodi:cy="241.25082"
|
ry="25.00013"
|
||||||
sodipodi:expansion="1"
|
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||||
sodipodi:revolution="9.8757124"
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264582px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
sodipodi:radius="19.917585"
|
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||||
sodipodi:argument="-17.350523"
|
id="path45-1"
|
||||||
sodipodi:t0="0.0044709877"
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
d="m 342.63471,241.338 c -0.14905,0.28891 -0.50558,-0.0176 -0.56257,-0.20833 -0.14974,-0.50126 0.34837,-0.9171 0.80488,-0.95318 0.77741,-0.0615 1.34953,0.67239 1.34379,1.40143 -0.008,1.04713 -0.99858,1.78883 -1.99798,1.7344 -1.31593,-0.0717 -2.23097,-1.32542 -2.12501,-2.59452 0.1323,-1.58458 1.65253,-2.6746 3.19107,-2.51563 1.85321,0.19148 3.11907,1.9798 2.90624,3.78762 -0.24981,2.12187 -2.30716,3.56409 -4.38417,3.29685 -2.39056,-0.30758 -4.00948,-2.63459 -3.68746,-4.98072 0.36499,-2.65927 2.96206,-4.45512 5.57726,-4.07807 2.92802,0.42215 4.90096,3.28957 4.46869,6.17381 -0.47911,3.19677 -3.61711,5.34694 -6.77036,4.8593 -3.46555,-0.53593 -5.79303,-3.94466 -5.24992,-7.36691 0.59265,-3.73433 4.27224,-6.2392 7.96347,-5.64052 4.00312,0.64926 6.68544,4.59982 6.03113,8.56001 -0.70581,4.27192 -4.92742,7.13174 -9.15656,6.42174 -4.54073,-0.76231 -7.57809,-5.25502 -6.81236,-9.7531 0.81876,-4.80955 5.58264,-8.02449 10.34966,-7.20298 5.07837,0.87517 8.4709,5.91026 7.59358,10.94621 -0.93154,5.3472 -6.23788,8.91735 -11.54275,7.98419 -5.61603,-0.98788 -9.36382,-6.56551 -8.37481,-12.1393 1.04421,-5.88486 6.89315,-9.81031 12.73585,-8.76542 6.1537,1.10051 10.25683,7.22079 9.15603,13.3324 -1.15679,6.42254 -7.54842,10.70335 -13.92894,9.54665 -6.69138,-1.21307 -11.14989,-7.87608 -9.93726,-14.5255 1.26932,-6.96022 8.20372,-11.59645 15.12205,-10.32787 7.22906,1.32556 12.043,8.53137 10.71848,15.7186 -1.38179,7.49791 -8.85902,12.48958 -16.31515,11.10909 -7.76676,-1.43801 -12.93616,-9.18668 -11.4997,-16.91169 1.49422,-8.03561 9.51433,-13.38276 17.50824,-11.89032 8.30446,1.55042 13.82935,9.84199 12.28093,18.10479 -1.60662,8.57331 -10.16965,14.27596 -18.70134,12.67154 -8.84216,-1.66281 -14.72257,-10.4973 -13.06215,-19.29789 1.71899,-9.11102 10.82496,-15.16918 19.89443,-13.45276 9.37988,1.77516 15.61581,11.15262 13.84338,20.49098 -1.83134,9.64873 -11.48029,16.06243 -21.08753,14.23399 -9.91759,-1.88751 -16.50906,-11.80795 -14.6246,-21.68408 1.94367,-10.18644 12.13561,-16.95569 22.28063,-15.01521 10.4553,1.99983 17.40232,12.46328 15.40582,22.87718 -0.63903,3.33322 -2.14652,6.47994 -4.33787,9.07108"
|
id="g37-4"
|
||||||
transform="translate(1.7875357,2.8617786)" /><g
|
inkscape:path-effect="#path-effect41-5"><path
|
||||||
id="g59"><path
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
d="m 344.4572,232.77302 c -1.73473,0.48665 -3.5256,1.055 -5.35265,1.69499 -1.25899,1.28881 -2.5256,2.67667 -3.78788,4.14445 -0.44591,1.74564 -0.84891,3.58112 -1.20819,5.48338 0.48665,1.73473 1.055,3.5256 1.69498,5.35265 1.28882,1.25899 2.67668,2.5256 4.14445,3.78788 1.74564,0.44591 3.58112,0.84892 5.48339,1.2082 1.73472,-0.48666 3.52559,-1.055 5.35264,-1.69499 1.25899,-1.28882 2.52561,-2.67668 3.78789,-4.14445 0.44591,-1.74564 0.84891,-3.5806 1.20819,-5.48287 -0.48665,-1.73472 -1.055,-3.52559 -1.69499,-5.35264 -1.28881,-1.25899 -2.67667,-2.52613 -4.14445,-3.7884 -1.74564,-0.44592 -3.58111,-0.84892 -5.48338,-1.2082 z"
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
style="fill:#26d734;fill-opacity:1;stroke:#9a9700;stroke-width:1.465;stroke-opacity:0.600002"
|
id="path36-6-4"
|
||||||
id="path42-5" /><path
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
sodipodi:type="spiral"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
style="fill:none;fill-rule:evenodd;stroke:#fffa32;stroke-width:1.065;stroke-dasharray:none;stroke-opacity:1"
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
id="path59"
|
id="path37-2-6-1-5-3"
|
||||||
sodipodi:cx="335.75626"
|
sodipodi:nodetypes="ccc"
|
||||||
sodipodi:cy="233.09792"
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
sodipodi:expansion="1"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
sodipodi:revolution="3"
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
sodipodi:radius="10.089354"
|
id="path37-2-6-4-6-0"
|
||||||
sodipodi:argument="-19.333729"
|
sodipodi:nodetypes="ccc"
|
||||||
sodipodi:t0="0"
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
d="m 335.75626,233.09792 c 0.44772,-0.23547 0.53829,0.48734 0.39136,0.74414 -0.39818,0.69589 -1.39291,0.51861 -1.87964,0.0386 -0.87067,-0.85865 -0.55945,-2.28928 0.31419,-3.01515 1.28209,-1.06524 3.19926,-0.60551 4.15065,0.66697 1.26805,1.69602 0.65405,4.11428 -1.01975,5.28616 -2.1063,1.47469 -5.03172,0.70392 -6.42166,-1.37253 -1.68339,-2.51485 -0.75456,-5.95055 1.7253,-7.55716 2.92244,-1.89333 6.87026,-0.8057 8.69267,2.07808 2.10404,3.32944 0.85716,7.79053 -2.43086,9.82817 -3.73605,2.31529 -8.71121,0.90885 -10.96367,-2.78363 -2.52692,-4.14241 -0.9607,-9.63219 3.13641,-12.09918 4.54858,-2.73883 10.55339,-1.01268 13.23468,3.48919"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
transform="matrix(0.8,0,0,0.8,76.234013,57.800034)" /></g></g></g></svg>
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-7"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(7.5790061e-6)" /></g><g
|
||||||
|
id="g42-1"
|
||||||
|
transform="rotate(-45,70.430131,928.10363)"><ellipse
|
||||||
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-0-9"
|
||||||
|
cx="-213.94203"
|
||||||
|
cy="831.42212"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||||
|
id="path45-1-7"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-9"
|
||||||
|
inkscape:path-effect="#path-effect41-8"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-2"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-0"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-6"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-9"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-4"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(-9.3315134e-6)" /></g><g
|
||||||
|
id="g63"><path
|
||||||
|
id="rect33-6"
|
||||||
|
style="fill:#005c01;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 57.136103,850.95756 -1.50032,-0.57183 -1.84004,1.853 3.34036,2.084 1.2e-4,13.42718 c 0,0 0.4451,1.28623 1.346,1.28623 0.83175,0 1.346,-1.28623 1.346,-1.28623 l -2.9e-4,-13.42718 2.71198,-2.084 -1.37887,-1.853 -1.33311,0.57183 z"
|
||||||
|
sodipodi:nodetypes="cccccscccccc" /><path
|
||||||
|
d="m 60.527143,867.65428 c 0,0 -1.28908,1.47478 -2.12083,1.47478 -0.9009,0 -1.99075,-1.42264 -1.99075,-1.42264 -4.65194,0.59664 -5.92716,3.58427 -8.28253,4.70926 -2.52584,1.20643 -11.53768,1.98812 -11.53779,3.7052 10e-6,2.41107 9.83202,4.36562 21.96041,4.36562 12.12839,0 21.96041,-1.95455 21.96042,-4.36562 10e-6,-1.68893 -9.37115,-3.14116 -11.61634,-3.7052 -2.57054,-0.64577 -3.5869,-4.17719 -8.37259,-4.7614 z"
|
||||||
|
style="fill:#8f4c00;stroke:#5c3100;stroke-linecap:round"
|
||||||
|
id="path57-3"
|
||||||
|
sodipodi:nodetypes="cscscsssc" /><g
|
||||||
|
id="g42-4-4"
|
||||||
|
transform="matrix(0.46393276,0.46393757,-0.46393276,0.46393757,360.22685,355.15504)"
|
||||||
|
style="stroke-width:1.52415;stroke-dasharray:none"><ellipse
|
||||||
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-0-8"
|
||||||
|
cx="640.62866"
|
||||||
|
cy="431.71231"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52415;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||||
|
id="path45-1-1"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-4-2"
|
||||||
|
inkscape:path-effect="#path-effect41-5-67"
|
||||||
|
style="stroke-width:1.52415;stroke-dasharray:none"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-4-9"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-3-3"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-0-9"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-7-0"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-8-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52415;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-8"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(7.5790061e-6)" /></g><g
|
||||||
|
id="g42-1-5"
|
||||||
|
transform="matrix(0.46393276,-0.46393276,0.46393276,0.46393276,-441.6633,553.4829)"
|
||||||
|
style="stroke-width:1.52416;stroke-dasharray:none"><ellipse
|
||||||
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-0-9-0"
|
||||||
|
cx="-213.94203"
|
||||||
|
cy="831.42212"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52416;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||||
|
id="path45-1-7-9"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-9-6"
|
||||||
|
inkscape:path-effect="#path-effect41-8-3"
|
||||||
|
style="stroke-width:1.52416;stroke-dasharray:none"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-2-3"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-0-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-6-5"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-8-6"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-9-1"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-4-1"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(-9.3315134e-6)" /></g></g></g></g></svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="64.542282mm"
|
||||||
|
height="80.020309mm"
|
||||||
|
viewBox="0 0 64.542282 80.020309"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-501.45474,-289.80731)"><g
|
||||||
|
id="g178-3"
|
||||||
|
transform="translate(19.691291,95.898306)"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874"><path
|
||||||
|
id="path175-7-0-7-6"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:1.99668;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 513.95892,195.94614 c -3.10481,5.52989 -13.31235,25.09794 -14.05905,44.58016 -0.40935,10.68044 10.72122,10.85122 14.09046,10.65924 3.36947,0.18812 14.50014,0.005 14.07863,-10.67474 -0.76889,-19.48135 -10.99894,-39.03835 -14.11004,-44.56466 z" /><g
|
||||||
|
id="g177-5"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874"><path
|
||||||
|
id="path175-7-0-4-6"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 496.88096,217.65547 c -3.11544,5.52938 -13.3579,25.09565 -14.10715,44.57609 -0.41075,10.67947 10.75789,10.85023 14.13867,10.65827 3.381,0.1881 14.54975,0.005 14.12679,-10.67377 -0.77152,-19.47957 -11.03656,-39.03478 -14.15831,-44.56059 z" /><path
|
||||||
|
id="path175-7-0-9-3"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 531.13647,217.65547 c -3.11544,5.52938 -13.3579,25.09565 -14.10715,44.57609 -0.41075,10.67947 10.75789,10.85023 14.13867,10.65827 3.381,0.1881 14.54975,0.005 14.12679,-10.67377 -0.77152,-19.47957 -11.03656,-39.03478 -14.15831,-44.56059 z" /></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="70.481842mm"
|
||||||
|
height="70.97554mm"
|
||||||
|
viewBox="0 0 70.481842 70.97554"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-483.44732,-408.61381)"><g
|
||||||
|
id="g182"><path
|
||||||
|
id="path182"
|
||||||
|
style="fill:#c29c00;fill-opacity:1;stroke:#755e00;stroke-width:1.4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 532.22935,429.6004 a 21,21 0 0 0 -21.00017,21.00017 21,21 0 0 0 21.00017,20.9998 21,21 0 0 0 20.99981,-20.9998 21,21 0 0 0 -20.99981,-21.00017 z m 0,7.0003 a 14,14 0 0 1 13.99987,13.99987 14,14 0 0 1 -13.99987,13.99987 14,14 0 0 1 -14.00023,-13.99987 14,14 0 0 1 14.00023,-13.99987 z" /><path
|
||||||
|
id="path178"
|
||||||
|
style="fill:#daaf00;fill-opacity:1;stroke:#755e00;stroke-width:2.21395;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 492.41992,423.0918 c -5.82792,15.71457 -3.5165,47.74793 4.98242,53.65625 1.6238,1.12884 6.50214,1.73437 15.82618,1.73437 9.32404,0 14.20041,-0.60553 15.82421,-1.73437 8.49892,-5.90832 10.81035,-37.94168 4.98243,-53.65625 h -20.80664 z" /><g
|
||||||
|
id="g181"
|
||||||
|
transform="translate(64.081238,-6.2578735)"
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-opacity:1"><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181"
|
||||||
|
cx="449.1463"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181-5"
|
||||||
|
cx="463.11505"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181-7"
|
||||||
|
cx="435.17755"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /></g><path
|
||||||
|
d="m 520.38364,409.61388 a 10,10 0 0 0 -8.32301,4.45657 10,10 0 0 0 -5.31647,-1.53065 10,10 0 0 0 -9.14414,5.95209 10,10 0 0 0 -3.15226,-0.50953 10,10 0 0 0 -10.00043,9.9999 10,10 0 0 0 10.00043,9.99991 10,10 0 0 0 1.16427,-0.0682 10,10 0 0 0 8.99893,5.63893 10,10 0 0 0 6.87503,-2.73833 10,10 0 0 0 7.97729,3.96978 10,10 0 0 0 9.41235,-6.62234 10,10 0 0 0 2.23759,0.25322 10,10 0 0 0 9.99991,-9.99991 10,10 0 0 0 -9.99991,-9.9999 10,10 0 0 0 -0.79789,0.032 10,10 0 0 0 -9.93169,-8.83357 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.9;stroke:#b2b2b2;stroke-width:2;stroke-opacity:0.7"
|
||||||
|
id="path180" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="81.560997mm"
|
||||||
|
height="48.303188mm"
|
||||||
|
viewBox="0 0 81.560997 48.303188"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-50.121599,-612.75355)"><g
|
||||||
|
id="g9"
|
||||||
|
style="stroke:#000000;stroke-opacity:1"><rect
|
||||||
|
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect6"
|
||||||
|
width="80"
|
||||||
|
height="20"
|
||||||
|
x="50.902103"
|
||||||
|
y="613.53406" /><rect
|
||||||
|
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect6-1"
|
||||||
|
width="80"
|
||||||
|
height="20"
|
||||||
|
x="50.902103"
|
||||||
|
y="640.27625" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 979 B |
|
|
@ -2,158 +2,29 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="24.399992mm"
|
width="29.583593mm"
|
||||||
height="24.399998mm"
|
height="29.755226mm"
|
||||||
viewBox="0 0 24.399992 24.399998"
|
viewBox="0 0 29.583593 29.755226"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
sodipodi:docname="icons.svg"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
id="namedview1"
|
id="defs1" /><g
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="true"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="2.8284272"
|
|
||||||
inkscape:cx="35.885668"
|
|
||||||
inkscape:cy="411.71291"
|
|
||||||
inkscape:window-width="1918"
|
|
||||||
inkscape:window-height="1042"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="17"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer4"><inkscape:grid
|
|
||||||
id="grid1"
|
|
||||||
units="mm"
|
|
||||||
originx="-1.6153444"
|
|
||||||
originy="-96.526486"
|
|
||||||
spacingx="0.26458333"
|
|
||||||
spacingy="0.26458334"
|
|
||||||
empcolor="#0099e5"
|
|
||||||
empopacity="0.30196078"
|
|
||||||
color="#0099e5"
|
|
||||||
opacity="0.14901961"
|
|
||||||
empspacing="5"
|
|
||||||
enabled="true"
|
|
||||||
visible="false" /><inkscape:page
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24.399992"
|
|
||||||
height="24.399998"
|
|
||||||
id="page2"
|
|
||||||
margin="0"
|
|
||||||
bleed="0" /></sodipodi:namedview><defs
|
|
||||||
id="defs1"><inkscape:path-effect
|
|
||||||
effect="mirror_symmetry"
|
|
||||||
start_point="50.372741,180.48552"
|
|
||||||
end_point="50.372741,189.138"
|
|
||||||
center_point="50.372741,184.81176"
|
|
||||||
id="path-effect166-2"
|
|
||||||
is_visible="true"
|
|
||||||
lpeversion="1.2"
|
|
||||||
lpesatellites=""
|
|
||||||
mode="free"
|
|
||||||
discard_orig_path="false"
|
|
||||||
fuse_paths="true"
|
|
||||||
oposite_fuse="false"
|
|
||||||
split_items="false"
|
|
||||||
split_open="false"
|
|
||||||
link_styles="false" /></defs><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer4"
|
id="layer4"
|
||||||
inkscape:label="Layer 4"
|
transform="translate(-151.13398,-751.15207)"><path
|
||||||
transform="translate(-1.6153444,-96.526492)"><g
|
d="m 165.17026,751.95407 c -4.4181,2.7e-4 -7.99953,3.58193 -7.99951,8.00003 0.006,2.34034 1.03726,4.56053 2.82102,6.0756 -1.78376,1.51507 -2.81464,3.73526 -2.82102,6.0756 -2e-5,4.4181 3.58141,7.99976 7.99951,8.00003 0.61448,5e-5 1.22714,-0.0711 1.82543,-0.21119 -3.61703,-0.84782 -6.17466,-4.07378 -6.17452,-7.78884 0.006,-2.34034 1.03726,-4.56053 2.82102,-6.0756 -1.78376,-1.51507 -2.81464,-3.73526 -2.82102,-6.0756 -1.4e-4,-3.71506 2.55749,-6.94114 6.17452,-7.78896 -0.59829,-0.14014 -1.21095,-0.21112 -1.82543,-0.21107 z"
|
||||||
id="g166-2"
|
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:1.604;stroke-opacity:1"
|
||||||
inkscape:path-effect="#path-effect166-2"
|
id="path36-1" /><path
|
||||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:1"
|
id="rect83-5-3"
|
||||||
transform="matrix(1.728,0,0,1.728,-72.928813,-210.62831)"
|
style="fill:#c04040;fill-opacity:1;stroke:#bf4141;stroke-width:0.399604;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:0.498353"
|
||||||
inkscape:export-filename="hunter.svg"
|
d="m 172.72687,766.02988 c 0.20968,1.88003 7.36137,4.2774 7.36137,2.18354 0,-0.43048 0.002,-1.62424 0,-2.18355 0.002,-0.55929 0,-1.75348 0,-2.18395 0,-2.09388 -7.15169,0.30393 -7.36137,2.18396 z" /><path
|
||||||
inkscape:export-xdpi="900.08"
|
id="path86-3"
|
||||||
inkscape:export-ydpi="900.08"><path
|
style="fill:#999999;fill-opacity:1;stroke:#000000;stroke-width:0.399604;stroke-opacity:0.498039"
|
||||||
id="path164-8"
|
d="m 161.23317,763.86155 -10.0552,2.26911 10.0552,2.2686 z" /><path
|
||||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
id="path88"
|
||||||
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
|
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:0.419304;stroke-opacity:1"
|
||||||
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
|
d="m 157.61727,765.84131 v 0.57818 h 22.12149 c -1.2e-4,-0.20698 2.3e-4,-0.4071 5.8e-4,-0.57818 z" /><path
|
||||||
sodipodi:type="star"
|
d="m 166.393,752.281 13.58876,13.749 -13.58376,13.75 c 0.14274,0.0419 0.28736,0.0802 0.43366,0.1145 L 180.5316,766.03 166.83166,752.16544 c -0.14795,0.0347 -0.29436,0.0731 -0.43866,0.11556 z"
|
||||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:0.264583px;stroke-opacity:1"
|
||||||
id="path165-3"
|
id="path39-7" /></g></svg>
|
||||||
inkscape:flatsided="false"
|
|
||||||
sodipodi:sides="2"
|
|
||||||
sodipodi:cx="51.634216"
|
|
||||||
sodipodi:cy="182.45293"
|
|
||||||
sodipodi:r1="1.3725731"
|
|
||||||
sodipodi:r2="0.82291079"
|
|
||||||
sodipodi:arg1="1.1071487"
|
|
||||||
sodipodi:arg2="2.677945"
|
|
||||||
inkscape:rounded="0.5"
|
|
||||||
inkscape:randomized="0"
|
|
||||||
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
|
|
||||||
inkscape:transform-center-x="0.12681959"
|
|
||||||
inkscape:transform-center-y="0.079724714"
|
|
||||||
transform="translate(-4.4662386,1.8414355)" /><path
|
|
||||||
sodipodi:type="star"
|
|
||||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
id="path165-6-8"
|
|
||||||
inkscape:flatsided="false"
|
|
||||||
sodipodi:sides="2"
|
|
||||||
sodipodi:cx="51.634216"
|
|
||||||
sodipodi:cy="182.45293"
|
|
||||||
sodipodi:r1="1.3725731"
|
|
||||||
sodipodi:r2="0.82291079"
|
|
||||||
sodipodi:arg1="1.1071487"
|
|
||||||
sodipodi:arg2="2.677945"
|
|
||||||
inkscape:rounded="0.5"
|
|
||||||
inkscape:randomized="0"
|
|
||||||
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
|
|
||||||
inkscape:transform-center-x="0.14863452"
|
|
||||||
inkscape:transform-center-y="0.01863483"
|
|
||||||
transform="rotate(25.009099,51.670619,176.27381)" /></g><g
|
|
||||||
id="g6"
|
|
||||||
inkscape:export-filename="../../src/werewolves/werewolves/img/hunter.svg"
|
|
||||||
inkscape:export-xdpi="900.08"
|
|
||||||
inkscape:export-ydpi="900.08"><path
|
|
||||||
id="path166-0"
|
|
||||||
style="fill:#0f07ff;fill-opacity:0.496669;stroke:#05009e;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
d="m 13.815426,98.72668 c -5.522776,5e-5 -10.000374,4.47713 -10.000423,9.99991 4.9e-5,5.52277 4.477647,9.99985 10.000423,9.9999 5.522777,-5e-5 9.999857,-4.47713 9.999907,-9.9999 -5e-5,-5.52278 -4.47713,-9.99986 -9.999907,-9.99991 z m 0,1.99988 c 4.418297,-2e-5 8.000047,3.58172 8.000027,8.00003 2e-5,4.4183 -3.58173,8.00004 -8.000027,8.00002 -4.418301,2e-5 -8.000045,-3.58172 -8.000029,-8.00002 -1.6e-5,-4.41831 3.581728,-8.00005 8.000029,-8.00003 z" /><rect
|
|
||||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
id="rect167-8-0-4"
|
|
||||||
width="1.5"
|
|
||||||
height="6"
|
|
||||||
x="-109.47657"
|
|
||||||
y="19.815336"
|
|
||||||
transform="rotate(-90)"
|
|
||||||
ry="0" /><rect
|
|
||||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
id="rect167-8-0-9-2"
|
|
||||||
width="1.5"
|
|
||||||
height="6"
|
|
||||||
x="-109.47657"
|
|
||||||
y="1.8153445"
|
|
||||||
transform="rotate(-90)"
|
|
||||||
ry="0" /><rect
|
|
||||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
id="rect167-8-0-1-2"
|
|
||||||
width="1.5"
|
|
||||||
height="6"
|
|
||||||
x="-14.56517"
|
|
||||||
y="-120.72649"
|
|
||||||
transform="scale(-1)"
|
|
||||||
ry="0" /><rect
|
|
||||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
|
||||||
id="rect167-8-0-1-7-2"
|
|
||||||
width="1.5"
|
|
||||||
height="6"
|
|
||||||
x="-14.56517"
|
|
||||||
y="-102.72649"
|
|
||||||
transform="scale(-1)"
|
|
||||||
ry="0" /></g></g></svg>
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
|
@ -2,9 +2,9 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="44.180328mm"
|
width="32.022381mm"
|
||||||
height="65.150642mm"
|
height="20.678709mm"
|
||||||
viewBox="0 0 44.180328 65.150642"
|
viewBox="0 0 32.022381 20.678709"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
|
@ -24,9 +24,9 @@
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:zoom="2"
|
inkscape:zoom="0.35355339"
|
||||||
inkscape:cx="93.5"
|
inkscape:cx="660.43774"
|
||||||
inkscape:cy="1629.25"
|
inkscape:cy="2131.2199"
|
||||||
inkscape:window-width="1918"
|
inkscape:window-width="1918"
|
||||||
inkscape:window-height="1042"
|
inkscape:window-height="1042"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
inkscape:current-layer="layer4"><inkscape:grid
|
inkscape:current-layer="layer4"><inkscape:grid
|
||||||
id="grid1"
|
id="grid1"
|
||||||
units="mm"
|
units="mm"
|
||||||
originx="-11.103428"
|
originx="-160.21693"
|
||||||
originy="-387.35551"
|
originy="-815.99713"
|
||||||
spacingx="0.26458333"
|
spacingx="0.26458333"
|
||||||
spacingy="0.26458334"
|
spacingy="0.26458334"
|
||||||
empcolor="#0099e5"
|
empcolor="#0099e5"
|
||||||
|
|
@ -47,71 +47,268 @@
|
||||||
enabled="true"
|
enabled="true"
|
||||||
visible="false" /><inkscape:page
|
visible="false" /><inkscape:page
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="-6.3560056e-12"
|
||||||
width="44.180328"
|
width="32.022381"
|
||||||
height="65.150642"
|
height="20.678711"
|
||||||
id="page2"
|
id="page2"
|
||||||
margin="0"
|
margin="0"
|
||||||
bleed="0" /></sodipodi:namedview><defs
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
id="defs1" /><g
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-5"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-5-67"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||||
|
effect="interpolate_points"
|
||||||
|
id="path-effect41-8-3"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
interpolator_type="CubicBezierJohan" /></defs><g
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer4"
|
id="layer4"
|
||||||
inkscape:label="Layer 4"
|
inkscape:label="Layer 4"
|
||||||
transform="translate(-11.103428,-387.3555)"><g
|
transform="translate(-160.21694,-815.99709)"><path
|
||||||
id="g113"
|
id="path82-8"
|
||||||
|
style="fill:#cccccc;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-opacity:0.7"
|
||||||
|
d="m 162.68403,816.49763 c -0.0381,-9.5e-4 -0.0769,-6.2e-4 -0.11679,0.002 -0.58468,0.0318 -0.9946,0.44045 -1.27951,0.92811 -0.38899,0.40947 -0.66926,0.91944 -0.53692,1.48983 0.31028,1.33731 2.14116,1.1325 2.88458,1.68207 v 5.73608 5.73815 c -0.74342,0.54957 -2.5743,0.34424 -2.88458,1.68155 -0.0248,0.10695 -0.0358,0.21228 -0.0336,0.31471 0.01,0.44389 0.25446,0.84295 0.57051,1.17564 0.28491,0.48766 0.69483,0.89632 1.27951,0.92811 1.2754,0.0694 1.63346,-1.49789 2.26911,-2.16834 h 11.39104 11.39258 c 0.63565,0.67045 0.99371,2.23774 2.26911,2.16834 0.58468,-0.0318 0.9946,-0.44045 1.27951,-0.92811 0.31605,-0.33269 0.56051,-0.73175 0.57051,-1.17564 0.002,-0.10243 -0.009,-0.20776 -0.0336,-0.31471 -0.31028,-1.33731 -2.14116,-1.13198 -2.88458,-1.68155 v -5.73815 -5.73608 c 0.74342,-0.54957 2.5743,-0.34476 2.88458,-1.68207 0.13234,-0.57039 -0.14793,-1.08036 -0.53692,-1.48983 -0.28491,-0.48766 -0.69483,-0.89631 -1.27951,-0.92811 -1.2754,-0.0694 -1.63346,1.49789 -2.26911,2.16834 h -11.39258 -11.39104 c -0.61579,-0.6495 -0.97116,-2.14045 -2.15232,-2.16989 z"
|
||||||
inkscape:export-filename="insomniac.svg"
|
inkscape:export-filename="insomniac.svg"
|
||||||
inkscape:export-xdpi="900.08"
|
inkscape:export-xdpi="900.08"
|
||||||
inkscape:export-ydpi="900.08"
|
inkscape:export-ydpi="900.08" /><g
|
||||||
style="stroke-width:1.3;stroke-dasharray:none"><path
|
id="g43"
|
||||||
id="path95-7"
|
transform="translate(0.07389574)"><g
|
||||||
style="fill:#b3b3b3;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
id="g42-4"
|
||||||
d="m 42.63286,411.37387 -30.86943,0.0811 v 36.01795 a 15.478125,4.2333331 0 0 0 -0.01,0.14986 15.478125,4.2333331 0 0 0 0.01,0.14935 v 0.015 h 0.002 a 15.478125,4.2333331 0 0 0 15.46675,4.06901 15.478125,4.2333331 0 0 0 15.47813,-4.23334 15.478125,4.2333331 0 0 0 -0.077,-0.42168 z" /><path
|
transform="matrix(0.70710678,0.70711411,-0.70710678,0.70711411,584.35218,19.075908)"><ellipse
|
||||||
id="path102"
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
id="path30-7-7-7-0"
|
||||||
d="m 42.63398,417.52803 v 3.99975 a 8,8 0 0 1 8.00003,8.00003 8,8 0 0 1 -8.00003,8.00003 v 3.99976 A 12,12 0 0 0 54.63376,429.52781 12,12 0 0 0 42.63398,417.52803 Z" /><g
|
cx="640.62866"
|
||||||
id="g105"
|
cy="431.71231"
|
||||||
transform="translate(-334.81852,-83.628473)"
|
rx="15.000077"
|
||||||
style="stroke-width:1.3;stroke-dasharray:none"><ellipse
|
ry="25.00013"
|
||||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||||
id="path95"
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264582px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
cx="362.05063"
|
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||||
cy="495.0354"
|
id="path45-1"
|
||||||
rx="15.478125"
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
ry="4.2333331" /><path
|
id="g37-4"
|
||||||
d="m 376.92945,495.93872 c -0.44231,-1.99962 -6.96407,-3.56361 -14.86007,-3.56361 -8.02391,-2e-5 -14.55676,1.54313 -14.8283,3.57717 1.94328,1.91824 7.94419,3.085 14.77662,3.08496 6.95042,10e-6 12.71785,-1.1183 14.91175,-3.09852 z"
|
inkscape:path-effect="#path-effect41-5"><path
|
||||||
style="fill:#804600;stroke:none;stroke-width:1.3;stroke-dasharray:none"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path105"
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
sodipodi:nodetypes="cscsc" /></g><g
|
id="path36-6-4"
|
||||||
id="g103"
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
transform="translate(-332.61358,-85.305623)"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
style="stroke-width:1.3;stroke-dasharray:none"><path
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
id="path37-2-6-1-5-3"
|
||||||
d="m 356.82899,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
sodipodi:nodetypes="ccc"
|
||||||
id="path103"
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
sodipodi:nodetypes="csc" /><path
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
d="m 363.70426,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
id="path37-2-6-4-6-0"
|
||||||
id="path103-2"
|
sodipodi:nodetypes="ccc"
|
||||||
sodipodi:nodetypes="csc" /><path
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
d="m 370.57952,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
id="path103-6"
|
id="path37-2-6-10-9-7"
|
||||||
sodipodi:nodetypes="csc" /></g><path
|
sodipodi:nodetypes="ccc"
|
||||||
sodipodi:type="star"
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
style="fill:none;fill-opacity:0;stroke:#4d2a00;stroke-width:3.17383;stroke-dasharray:none;stroke-opacity:0.698282"
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path84"
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
inkscape:flatsided="false"
|
id="path37-2-6-6-3-8"
|
||||||
sodipodi:sides="5"
|
sodipodi:nodetypes="ccc"
|
||||||
sodipodi:cx="358.90729"
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
sodipodi:cy="61.780209"
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
sodipodi:r1="16.631435"
|
id="path30-7-7-7"
|
||||||
sodipodi:r2="24.204502"
|
cx="213.9375"
|
||||||
sodipodi:arg1="-2.7064071"
|
cy="831.42084"
|
||||||
sodipodi:arg2="0.21005099"
|
rx="15"
|
||||||
inkscape:rounded="0.6"
|
ry="25"
|
||||||
inkscape:randomized="0"
|
transform="translate(7.5790061e-6)" /></g><g
|
||||||
d="m 343.82604,54.76875 c 9.88839,22.253806 17.47085,23.892638 38.75374,12.058334 16.02631,-8.911392 -8.02828,-33.81664 -21.66456,-21.556656 -18.10895,16.281225 -17.32446,23.999 0.50741,40.58323 13.42764,12.488151 29.68066,-18.085265 13.80688,-27.265597 -21.08034,-12.191455 -28.17796,-9.06044 -38.44015,13.023483 -7.72757,16.629493 26.37194,22.63933 30.19768,4.70559 5.08058,-23.815959 -0.0905,-29.598661 -24.26472,-32.534276 -18.20354,-2.210558 -13.38191,32.077141 4.85631,30.173812 24.22031,-2.527617 28.12204,-9.232538 23.44372,-33.130771 -3.52283,-17.995694 -34.64241,-2.814567 -27.19631,13.942851 z"
|
id="g42-1"
|
||||||
transform="matrix(0.40959999,0,0,0.40959999,-120.0474,406.73268)" /><path
|
transform="rotate(-45,70.430131,928.10363)"><ellipse
|
||||||
d="m 26.89965,426.0882 c -0.667117,0.62908 -1.259228,1.22677 -1.777669,1.80041 -1.449367,0.19777 -2.672199,0.97551 -3.838526,2.36885 0.392134,0.82886 0.777882,1.57708 1.163237,2.24741 -0.259786,1.43954 0.101868,2.84235 1.066601,4.38216 0.909469,-0.11681 1.740402,-0.252 2.497006,-0.41135 1.288809,0.69192 2.734836,0.78121 4.4974,0.33952 0.169949,-0.90105 0.29757,-1.73284 0.379821,-2.50166 1.056314,-1.01192 1.588482,-2.35957 1.713074,-4.17235 -0.804435,-0.44008 -1.556108,-0.8185 -2.261877,-1.1343 -0.635972,-1.31732 -1.753505,-2.24001 -3.439067,-2.91869 z"
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
style="fill:#804600;fill-opacity:0.701315;stroke:none;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:0.497152"
|
id="path30-7-7-7-0-9"
|
||||||
id="path113" /></g></g></svg>
|
cx="-213.94203"
|
||||||
|
cy="831.42212"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||||
|
id="path45-1-7"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-9"
|
||||||
|
inkscape:path-effect="#path-effect41-8"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-2"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-0"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-6"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-9"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-4"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(-9.3315134e-6)" /></g><g
|
||||||
|
id="g63"
|
||||||
|
inkscape:export-filename="apprentice.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08"><path
|
||||||
|
id="rect33-6"
|
||||||
|
style="fill:#005c01;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 57.136103,850.95756 -1.50032,-0.57183 -1.84004,1.853 3.34036,2.084 1.2e-4,13.42718 c 0,0 0.4451,1.28623 1.346,1.28623 0.83175,0 1.346,-1.28623 1.346,-1.28623 l -2.9e-4,-13.42718 2.71198,-2.084 -1.37887,-1.853 -1.33311,0.57183 z"
|
||||||
|
sodipodi:nodetypes="cccccscccccc" /><path
|
||||||
|
d="m 60.527143,867.65428 c 0,0 -1.28908,1.47478 -2.12083,1.47478 -0.9009,0 -1.99075,-1.42264 -1.99075,-1.42264 -4.65194,0.59664 -5.92716,3.58427 -8.28253,4.70926 -2.52584,1.20643 -11.53768,1.98812 -11.53779,3.7052 10e-6,2.41107 9.83202,4.36562 21.96041,4.36562 12.12839,0 21.96041,-1.95455 21.96042,-4.36562 10e-6,-1.68893 -9.37115,-3.14116 -11.61634,-3.7052 -2.57054,-0.64577 -3.5869,-4.17719 -8.37259,-4.7614 z"
|
||||||
|
style="fill:#8f4c00;stroke:#5c3100;stroke-linecap:round"
|
||||||
|
id="path57-3"
|
||||||
|
sodipodi:nodetypes="cscscsssc" /><g
|
||||||
|
id="g42-4-4"
|
||||||
|
transform="matrix(0.46393276,0.46393757,-0.46393276,0.46393757,360.22685,355.15504)"
|
||||||
|
style="stroke-width:1.52415;stroke-dasharray:none"><ellipse
|
||||||
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-0-8"
|
||||||
|
cx="640.62866"
|
||||||
|
cy="431.71231"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52415;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||||
|
id="path45-1-1"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-4-2"
|
||||||
|
inkscape:path-effect="#path-effect41-5-67"
|
||||||
|
style="stroke-width:1.52415;stroke-dasharray:none"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-4-9"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-3-3"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-0-9"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-7-0"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-8-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52415;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-8"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(7.5790061e-6)" /></g><g
|
||||||
|
id="g42-1-5"
|
||||||
|
transform="matrix(0.46393276,-0.46393276,0.46393276,0.46393276,-441.6633,553.4829)"
|
||||||
|
style="stroke-width:1.52416;stroke-dasharray:none"><ellipse
|
||||||
|
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-7-0-9-0"
|
||||||
|
cx="-213.94203"
|
||||||
|
cy="831.42212"
|
||||||
|
rx="15.000077"
|
||||||
|
ry="25.00013"
|
||||||
|
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||||
|
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52416;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||||
|
id="path45-1-7-9"
|
||||||
|
sodipodi:nodetypes="sssccs" /><g
|
||||||
|
id="g37-9-6"
|
||||||
|
inkscape:path-effect="#path-effect41-8-3"
|
||||||
|
style="stroke-width:1.52416;stroke-dasharray:none"><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||||
|
id="path36-6-2-3"
|
||||||
|
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-1-5-0-8"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-4-6-6-5"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||||
|
id="path37-2-6-10-9-8-6"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||||
|
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||||
|
id="path37-2-6-6-3-9-1"
|
||||||
|
sodipodi:nodetypes="ccc"
|
||||||
|
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path30-7-7-4-1"
|
||||||
|
cx="213.9375"
|
||||||
|
cy="831.42084"
|
||||||
|
rx="15"
|
||||||
|
ry="25"
|
||||||
|
transform="translate(-9.3315134e-6)" /></g></g></g><g
|
||||||
|
id="g100-5"
|
||||||
|
transform="matrix(1.771561,0,0,1.771561,-126.11368,-663.87619)"><path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path89-4-9"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="170.66406"
|
||||||
|
sodipodi:cy="-843.37482"
|
||||||
|
sodipodi:rx="2"
|
||||||
|
sodipodi:ry="2"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:end="3.1415927"
|
||||||
|
d="m 172.66406,-843.37482 a 2,2 0 0 1 -1,1.73205 2,2 0 0 1 -2,0 2,2 0 0 1 -1,-1.73205"
|
||||||
|
sodipodi:open="true"
|
||||||
|
transform="scale(1,-1)" /><g
|
||||||
|
id="g97-4"><circle
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path90-6"
|
||||||
|
cx="169.00261"
|
||||||
|
cy="839.49762"
|
||||||
|
r="1" /><circle
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path90-9-9"
|
||||||
|
cx="172.32553"
|
||||||
|
cy="839.49762"
|
||||||
|
r="1" /></g></g></g></svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="81.560997mm"
|
||||||
|
height="79.989365mm"
|
||||||
|
viewBox="0 0 81.560997 79.989365"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-50.121599,-596.91049)"><g
|
||||||
|
id="g9"
|
||||||
|
style="stroke:#000000;stroke-opacity:1"><rect
|
||||||
|
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect6"
|
||||||
|
width="80"
|
||||||
|
height="20"
|
||||||
|
x="50.902103"
|
||||||
|
y="613.53406" /><rect
|
||||||
|
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect6-1"
|
||||||
|
width="80"
|
||||||
|
height="20"
|
||||||
|
x="50.902103"
|
||||||
|
y="640.27625" /></g><rect
|
||||||
|
style="fill:#ff0707;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect6-3"
|
||||||
|
width="100"
|
||||||
|
height="10"
|
||||||
|
x="-436.08246"
|
||||||
|
y="509.63745"
|
||||||
|
transform="rotate(-45)" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -2,94 +2,26 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="22.945572mm"
|
width="22.945564mm"
|
||||||
height="28.24629mm"
|
height="28.246271mm"
|
||||||
viewBox="0 0 22.945572 28.24629"
|
viewBox="0 0 22.945564 28.246271"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
sodipodi:docname="icons.svg"
|
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="true"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="2.8284272"
|
|
||||||
inkscape:cx="329.68853"
|
|
||||||
inkscape:cy="1037.856"
|
|
||||||
inkscape:window-width="1918"
|
|
||||||
inkscape:window-height="1042"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="17"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer4"><inkscape:grid
|
|
||||||
id="grid1"
|
|
||||||
units="mm"
|
|
||||||
originx="-84.294216"
|
|
||||||
originy="-256.51758"
|
|
||||||
spacingx="0.26458333"
|
|
||||||
spacingy="0.26458334"
|
|
||||||
empcolor="#0099e5"
|
|
||||||
empopacity="0.30196078"
|
|
||||||
color="#0099e5"
|
|
||||||
opacity="0.14901961"
|
|
||||||
empspacing="5"
|
|
||||||
enabled="true"
|
|
||||||
visible="false" /><inkscape:page
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="22.945572"
|
|
||||||
height="28.24629"
|
|
||||||
id="page2"
|
|
||||||
margin="0"
|
|
||||||
bleed="0" /></sodipodi:namedview><defs
|
|
||||||
id="defs1" /><g
|
id="defs1" /><g
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer4"
|
id="layer4"
|
||||||
inkscape:label="Layer 4"
|
transform="translate(-403.75417,-318.29373)"><g
|
||||||
transform="translate(-84.29421,-256.51757)"><g
|
id="g28"><path
|
||||||
id="g17"><path
|
id="path13-2-7"
|
||||||
id="path13-2"
|
|
||||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
d="m 102.26758,259.22827 -4.652431,14.9748 c 0,0 -3.853406,-2.83967 -6.441467,-7.23625 -0.281201,4.36457 0.45945,18.75999 11.093898,19.05465 10.63444,-0.29466 11.37665,-14.69008 11.09545,-19.05465 -2.58806,4.39658 -6.44097,7.23625 -6.44096,7.23625 z m -6.895188,20.4246 h 4.976438 a 2.48832,2.48832 0 0 1 -2.488217,2.48822 2.48832,2.48832 0 0 1 -2.488221,-2.48822 z m 8.815478,0 h 4.97645 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48823,-2.48822 z"
|
d="m 415.2262,319.4142 -4.65243,14.9748 c 0,0 -3.85341,-2.83967 -6.44147,-7.23625 -0.2812,4.36457 0.45945,18.75999 11.0939,19.05465 10.63444,-0.29466 11.37665,-14.69008 11.09545,-19.05465 -2.58806,4.39658 -6.44097,7.23625 -6.44096,7.23625 z m -6.89519,20.4246 h 4.97644 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48822,-2.48822 z m 8.81548,0 h 4.97645 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48823,-2.48822 z" /><path
|
||||||
transform="translate(-6.5013311,-1.5902536)" /><path
|
|
||||||
sodipodi:type="star"
|
|
||||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path14"
|
id="path14-5"
|
||||||
inkscape:flatsided="false"
|
transform="translate(345.42786,67.661219)"
|
||||||
sodipodi:sides="3"
|
d="m 64.171412,262.11122 c -0.371671,0.14644 -0.815095,-1.38393 -1.090057,-1.67373 -0.307066,-0.32362 -1.843044,-0.90688 -1.777137,-1.34811 0.05902,-0.3951 1.606071,-0.0139 1.994519,-0.10715 0.433804,-0.10412 1.706909,-1.14268 2.05607,-0.86499 0.312654,0.24866 -0.790976,1.39786 -0.904463,1.78088 -0.126738,0.42774 0.136135,2.04957 -0.278932,2.2131 z" /><path
|
||||||
sodipodi:cx="63.610146"
|
|
||||||
sodipodi:cy="259.77261"
|
|
||||||
sodipodi:r1="2.4050174"
|
|
||||||
sodipodi:r2="0.8495208"
|
|
||||||
sodipodi:arg1="1.3352513"
|
|
||||||
sodipodi:arg2="2.2426747"
|
|
||||||
inkscape:rounded="0.2"
|
|
||||||
inkscape:randomized="0"
|
|
||||||
d="m 64.171412,262.11122 c -0.371671,0.14644 -0.815095,-1.38393 -1.090057,-1.67373 -0.307066,-0.32362 -1.843044,-0.90688 -1.777137,-1.34811 0.05902,-0.3951 1.606071,-0.0139 1.994519,-0.10715 0.433804,-0.10412 1.706909,-1.14268 2.05607,-0.86499 0.312654,0.24866 -0.790976,1.39786 -0.904463,1.78088 -0.126738,0.42774 0.136135,2.04957 -0.278932,2.2131 z"
|
|
||||||
transform="translate(25.967913,5.8850319)" /><path
|
|
||||||
sodipodi:type="star"
|
|
||||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path15"
|
id="path15-9"
|
||||||
inkscape:flatsided="false"
|
transform="translate(348.34768,68.803695)"
|
||||||
sodipodi:sides="4"
|
d="m 74.180658,260.24033 c -0.269428,0.0674 -0.796295,-0.78347 -1.034437,-0.92636 -0.238143,-0.14288 -1.236814,-0.20738 -1.304171,-0.4768 -0.06736,-0.26943 0.783475,-0.7963 0.92636,-1.03444 0.142886,-0.23814 0.207377,-1.23681 0.476805,-1.30417 0.269427,-0.0674 0.796295,0.78347 1.034437,0.92636 0.238143,0.14289 1.236814,0.20738 1.304171,0.4768 0.06736,0.26943 -0.783475,0.7963 -0.926361,1.03444 -0.142885,0.23814 -0.207376,1.23682 -0.476804,1.30417 z" /></g></g></svg>
|
||||||
sodipodi:cx="73.712936"
|
|
||||||
sodipodi:cy="258.36945"
|
|
||||||
sodipodi:r1="1.9284658"
|
|
||||||
sodipodi:r2="1.1014973"
|
|
||||||
sodipodi:arg1="1.3258177"
|
|
||||||
sodipodi:arg2="2.1112159"
|
|
||||||
inkscape:rounded="0.2"
|
|
||||||
inkscape:randomized="0"
|
|
||||||
d="m 74.180658,260.24033 c -0.269428,0.0674 -0.796295,-0.78347 -1.034437,-0.92636 -0.238143,-0.14288 -1.236814,-0.20738 -1.304171,-0.4768 -0.06736,-0.26943 0.783475,-0.7963 0.92636,-1.03444 0.142886,-0.23814 0.207377,-1.23681 0.476805,-1.30417 0.269427,-0.0674 0.796295,0.78347 1.034437,0.92636 0.238143,0.14289 1.236814,0.20738 1.304171,0.4768 0.06736,0.26943 -0.783475,0.7963 -0.926361,1.03444 -0.142885,0.23814 -0.207376,1.23682 -0.476804,1.30417 z"
|
|
||||||
transform="translate(28.887732,7.0275078)" /></g></g></svg>
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="87.060303mm"
|
||||||
|
height="87.060318mm"
|
||||||
|
viewBox="0 0 87.060303 87.060318"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-207.43333,-614.36255)"><path
|
||||||
|
id="rect6-3-3"
|
||||||
|
style="fill:#ff0707;fill-opacity:1;stroke:#c10000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m -277.74226,592.65854 h -20.00022 v 39.9997 h -39.99971 v 20.00022 h 39.99971 v 39.9997 h 20.00022 v -39.9997 h 39.9997 v -20.00022 h -39.9997 z"
|
||||||
|
transform="rotate(-45)" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 776 B |
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="63.999481mm"
|
||||||
|
height="58.622898mm"
|
||||||
|
viewBox="0 0 63.999481 58.622898"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-494.49485,-99.66878)"><g
|
||||||
|
id="g174"><path
|
||||||
|
id="path31-2"
|
||||||
|
style="fill:#d77fff;fill-opacity:1;stroke:#a300eb;stroke-width:1.561"
|
||||||
|
d="m 497.38435,113.0436 a 29.821014,23.755724 75 0 0 -0.8444,21.15361 29.821014,23.755724 75 0 0 30.66443,22.65638 l -2.71657,-10.13837 a 16.30047,20.596718 75 0 0 -9.05328,5.10293 9.0979366,10.867 75 0 1 7.93756,-9.26688 l -8.20534,-30.62273 a 14.278707,28.051973 75 0 1 -17.7824,1.11506 z m 9.24862,11.76284 a 9.3881665,4.5422098 75 0 1 5.81712,5.06988 16.820465,8.6090562 75 0 0 -4.41462,-0.85597 16.820465,8.6090562 75 0 0 -3.81591,3.81033 9.3881665,4.5422098 75 0 1 2.41341,-8.02424 z" /><path
|
||||||
|
id="path32-7"
|
||||||
|
style="fill:#d77fff;fill-opacity:1;stroke:#a300eb;stroke-width:1.561"
|
||||||
|
d="m 555.88352,102.49759 a 28.051973,14.278707 15 0 1 -17.08676,-1.58883 28.051973,14.278707 15 0 1 -0.97402,-0.27031 l -8.92704,33.31616 a 20.596718,16.30047 15 0 0 0.34214,0.0948 20.596718,16.30047 15 0 0 9.83564,0.65972 10.867,9.0979366 15 0 1 -11.28974,3.39538 l -1.99873,7.45939 a 23.755724,29.821014 15 0 0 30.66441,-22.65638 23.755724,29.821014 15 0 0 -0.56609,-20.40922 z m -9.43994,11.04123 a 4.5422098,9.3881665 15 0 1 2.50283,7.2992 8.6090562,16.820465 15 0 0 -3.39532,-2.9481 8.6090562,16.820465 15 0 0 -5.21033,1.39175 4.5422098,9.3881665 15 0 1 6.10282,-5.74285 z" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -17,6 +17,10 @@ $offensive_color: color.adjust($village_color, $hue: 30deg);
|
||||||
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||||
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
||||||
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
||||||
|
$traitor_color: color.adjust($village_color, $hue: 45deg);
|
||||||
|
$traitor_border: color.change($traitor_color, $alpha: 1.0);
|
||||||
|
$drunk_color: color.adjust($village_color, $hue: 150deg);
|
||||||
|
$drunk_border: color.change($drunk_color, $alpha: 1.0);
|
||||||
|
|
||||||
$wolves_border_faint: color.change($wolves_border, $alpha: 0.3);
|
$wolves_border_faint: color.change($wolves_border, $alpha: 0.3);
|
||||||
$village_border_faint: color.change($village_border, $alpha: 0.3);
|
$village_border_faint: color.change($village_border, $alpha: 0.3);
|
||||||
|
|
@ -24,6 +28,8 @@ $offensive_border_faint: color.change($offensive_border, $alpha: 0.3);
|
||||||
$defensive_border_faint: color.change($defensive_border, $alpha: 0.3);
|
$defensive_border_faint: color.change($defensive_border, $alpha: 0.3);
|
||||||
$intel_border_faint: color.change($intel_border, $alpha: 0.3);
|
$intel_border_faint: color.change($intel_border, $alpha: 0.3);
|
||||||
$starts_as_villager_border_faint: color.change($starts_as_villager_border, $alpha: 0.3);
|
$starts_as_villager_border_faint: color.change($starts_as_villager_border, $alpha: 0.3);
|
||||||
|
$traitor_border_faint: color.change($traitor_border, $alpha: 0.3);
|
||||||
|
$drunk_border_faint: color.change($drunk_border, $alpha: 0.3);
|
||||||
|
|
||||||
$wolves_color_faint: color.change($wolves_color, $alpha: 0.1);
|
$wolves_color_faint: color.change($wolves_color, $alpha: 0.1);
|
||||||
$village_color_faint: color.change($village_color, $alpha: 0.1);
|
$village_color_faint: color.change($village_color, $alpha: 0.1);
|
||||||
|
|
@ -31,7 +37,8 @@ $offensive_color_faint: color.change($offensive_color, $alpha: 0.1);
|
||||||
$defensive_color_faint: color.change($defensive_color, $alpha: 0.1);
|
$defensive_color_faint: color.change($defensive_color, $alpha: 0.1);
|
||||||
$intel_color_faint: color.change($intel_color, $alpha: 0.1);
|
$intel_color_faint: color.change($intel_color, $alpha: 0.1);
|
||||||
$starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha: 0.1);
|
$starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha: 0.1);
|
||||||
|
$traitor_color_faint: color.change($traitor_color, $alpha: 0.1);
|
||||||
|
$drunk_color_faint: color.change($drunk_color, $alpha: 0.1);
|
||||||
|
|
||||||
@mixin flexbox() {
|
@mixin flexbox() {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -102,7 +109,7 @@ app {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2.7vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
$link_color: #432054;
|
$link_color: #432054;
|
||||||
|
|
@ -280,7 +287,6 @@ nav.host-nav {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
// background-color: hsl(283, 100%, 80%);
|
|
||||||
border: 1px solid rgba(0, 255, 0, 0.7);
|
border: 1px solid rgba(0, 255, 0, 0.7);
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: rgba(0, 255, 0, 0.7);
|
color: rgba(0, 255, 0, 0.7);
|
||||||
|
|
@ -298,6 +304,7 @@ nav.host-nav {
|
||||||
border: 1px solid rgba(255, 0, 0, 1);
|
border: 1px solid rgba(255, 0, 0, 1);
|
||||||
color: rgba(255, 0, 0, 1);
|
color: rgba(255, 0, 0, 1);
|
||||||
filter: none;
|
filter: none;
|
||||||
|
background-color: rgba(255, 0, 0, 0.1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 0, 0, 0.3);
|
background-color: rgba(255, 0, 0, 0.3);
|
||||||
|
|
@ -383,15 +390,38 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity {
|
.identity {
|
||||||
font-size: 1.5em;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.day-char {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
// min-width: 1vw;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.character {
|
.character {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 3px solid rgba(0, 0, 0, 0.4);
|
border: 3px solid rgba(0, 0, 0, 0.4);
|
||||||
// min-width: 20%;
|
// min-width: 20%;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
@ -634,19 +664,22 @@ clients {
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
color: black;
|
color: black;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-reveal-card {
|
.role-reveal-card {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 5cm;
|
// min-width: 5cm;
|
||||||
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 1cm;
|
padding: 20px;
|
||||||
border: 1px solid $wolves_color;
|
border: 1px solid $wolves_color;
|
||||||
background-color: color.change($wolves_color, $alpha: 0.1);
|
background-color: color.change($wolves_color, $alpha: 0.1);
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|
@ -751,7 +784,6 @@ clients {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.sp-ace {
|
.sp-ace {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -788,12 +820,9 @@ clients {
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-nav {
|
.client-nav {
|
||||||
// position: absolute;
|
|
||||||
// left: 0;
|
|
||||||
// top: 0;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
// background-color: rgba(255, 107, 255, 0.2);
|
height: 37px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: baseline;
|
justify-content: baseline;
|
||||||
|
|
@ -881,6 +910,7 @@ error {
|
||||||
.binary {
|
.binary {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-size: 1.5vw;
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -924,6 +954,11 @@ input {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
background-color: #000;
|
||||||
|
min-width: 2cm;
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -1088,15 +1123,20 @@ input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&>.submenu {
|
&>.submenu {
|
||||||
min-width: 30vw;
|
width: 30vw;
|
||||||
|
// position: absolute;
|
||||||
|
|
||||||
.assign-list {
|
.assign-list {
|
||||||
// min-width: 5cm;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
& .submenu button {
|
& .submenu button {
|
||||||
width: 5cm;
|
width: 10vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1104,6 +1144,7 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1126,6 +1167,13 @@ input {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-fit {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-15pct {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
.village {
|
.village {
|
||||||
background-color: $village_color;
|
background-color: $village_color;
|
||||||
|
|
@ -1241,6 +1289,44 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.traitor {
|
||||||
|
background-color: $traitor_color;
|
||||||
|
border: 1px solid $traitor_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $traitor_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $traitor_border_faint;
|
||||||
|
background-color: $traitor_color_faint;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $traitor_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drunk {
|
||||||
|
background-color: $drunk_color;
|
||||||
|
border: 1px solid $drunk_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $drunk_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $drunk_border_faint;
|
||||||
|
background-color: $drunk_color_faint;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $drunk_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.assignments {
|
.assignments {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -1281,9 +1367,12 @@ input {
|
||||||
|
|
||||||
.setup-screen {
|
.setup-screen {
|
||||||
margin-top: 2%;
|
margin-top: 2%;
|
||||||
font-size: 1rem;
|
font-size: 1.5vw;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
.setup {
|
.setup {
|
||||||
|
height: 85%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -1299,6 +1388,7 @@ input {
|
||||||
|
|
||||||
&.final {
|
&.final {
|
||||||
margin-top: 1cm;
|
margin-top: 1cm;
|
||||||
|
margin-bottom: 1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .title {
|
& .title {
|
||||||
|
|
@ -1341,6 +1431,7 @@ input {
|
||||||
filter: saturate(40%);
|
filter: saturate(40%);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
&.wakes {
|
&.wakes {
|
||||||
border: 2px solid yellow;
|
border: 2px solid yellow;
|
||||||
|
|
@ -1351,6 +1442,23 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.change,
|
||||||
|
li.choice {
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.li-icon {
|
||||||
|
filter: brightness(5000%);
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop-filter: invert(15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.li-icon {
|
||||||
|
filter: grayscale(100%) brightness(150%);
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
@ -1361,7 +1469,8 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive {
|
.inactive {
|
||||||
filter: grayscale(100%) brightness(30%);
|
// filter: grayscale(100%) brightness(30%);
|
||||||
|
filter: brightness(0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode {
|
.qrcode {
|
||||||
|
|
@ -1383,6 +1492,7 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
font-size: 5vw;
|
||||||
// height: 100%;
|
// height: 100%;
|
||||||
// width: 100%;
|
// width: 100%;
|
||||||
border: 1px solid $village_border;
|
border: 1px solid $village_border;
|
||||||
|
|
@ -1485,12 +1595,17 @@ input {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.full-height {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&#number {
|
#number {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
max-width: 50vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1724,12 +1839,7 @@ input {
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
// align-self: flex-start;
|
// align-self: flex-start;
|
||||||
}
|
font-size: 4vw;
|
||||||
|
|
||||||
.information {
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding-left: 5%;
|
|
||||||
padding-right: 5%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
|
|
@ -1754,6 +1864,31 @@ input {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-icon-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon-list-grow {
|
||||||
|
padding: 20px 0 20px 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-player-list {
|
.info-player-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -1763,20 +1898,33 @@ input {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
&.masons {
|
&.masons,
|
||||||
font-size: 2em;
|
&.large {
|
||||||
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.two-column {
|
.two-column {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seer-check {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-title-span {
|
.role-title-span {
|
||||||
display: grid;
|
// display: grid;
|
||||||
grid-template-columns: 1fr 100fr;
|
// grid-template-columns: 1fr 100fr;
|
||||||
max-height: 2rem;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 2rem;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1784,11 +1932,18 @@ input {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
max-height: 2rem;
|
max-height: 2rem;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -1798,6 +1953,7 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -1806,6 +1962,7 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-player {
|
.add-player {
|
||||||
|
|
@ -1838,3 +1995,227 @@ input {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.victory {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 3vw;
|
||||||
|
height: max-content;
|
||||||
|
gap: 1cm;
|
||||||
|
padding: 1cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.end-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.big-screen {
|
||||||
|
--information-height: 75vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not(.big-screen) {
|
||||||
|
--information-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
height: var(--information-height);
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-aura {
|
||||||
|
&.active {
|
||||||
|
$active_color: color.change($connected_color, $alpha: 0.7);
|
||||||
|
border: $active_color 1px solid;
|
||||||
|
color: $active_color;
|
||||||
|
background-color: color.change($active_color, $alpha: 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $active_color;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aura-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
// text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guardian-select {
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-text {
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
z-index: 5;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.dialog-box {
|
||||||
|
border: 1px solid white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&>button {
|
||||||
|
min-width: 4cm;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: hsl(280, 65%, 43%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.redir-url {
|
||||||
|
user-select: text;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 1cm;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #000;
|
||||||
|
z-index: 3;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
|
border-top: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-confirm {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||