mirror of
https://github.com/unanmed/ginka-generator.git
synced 2026-06-10 17:11:09 +08:00
init: Minamo Model
This commit is contained in:
commit
9f62be3736
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
result
|
||||
node_modules
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
*.js
|
||||
13
.prettierrc
Normal file
13
.prettierrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"bracketSpacing": true,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "none",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python 调试程序: 模块",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "ginka.topology.test"
|
||||
}
|
||||
]
|
||||
}
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
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) 2025 <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/>.
|
||||
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# GINKA 地图生成器
|
||||
|
||||
GINKA Model 是一个用于生成网格状魔塔地图的模型,采用 UNet 网络,允许输入自然语言,指定地图大小。
|
||||
|
||||
GINKA Model 内部集成了 Minamo Model 用于判别两个地图的相似性,用于计算损失值,指导 GINKA Model 训练,避免了传统拓扑图相似度计算的不可微性质,提高模型训练性能。
|
||||
|
||||
## 贡献 GINKA Model 数据集
|
||||
|
||||
对于 HTML5 魔塔,如果你想要贡献数据集,需要对你的魔塔进行手动数据处理,流程如下:
|
||||
|
||||
1. 选择楼层,可以是剧情层、战斗层等,但是需要满足下述条件
|
||||
2. 楼层除边缘外不应出现墙壁堆叠(例如 2\*2,边缘可以有重叠)
|
||||
3. 楼层中不应该有闲置怪,不应该有连续 3 个以上的怪物,不应该有无法到达的区域,不宜有过多的入口
|
||||
4. 最外面一层围上一圈墙壁(箭头楼层切换除外)
|
||||
5. 将所有的墙壁换成黄墙(数字 1)
|
||||
6. 将所有的血瓶换成红血瓶(数字 31),所有红宝石换成最基础的红宝石(数字 27),蓝宝石换成最基础的蓝宝石(数字 28),删除除此之外的资源,剑盾可以当成红蓝宝石看待
|
||||
7. 所有钥匙换成黄钥匙(数字 21),所有门换成黄门(数字 81)
|
||||
8. 所有箭头换成样板原版箭头(数字 161 至 164),所有上下楼梯换成样板原版楼梯(数字 87 和 88)
|
||||
9. 怪物分为三个强度,弱怪,中怪,强怪,弱怪换为绿头怪(数字 201),中怪换成红头怪(数字 202),强怪换成青头怪(数字 203)
|
||||
10. 在 `project` 文件夹下创建 `ginka-config.json` 文件,双击进入编辑,粘贴如下模板:
|
||||
|
||||
```json
|
||||
{
|
||||
"clip": {
|
||||
"defaults": [0, 0, 13, 13],
|
||||
"special": {
|
||||
"MT11": [3, 3, 7, 7]
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"MT1": ["MT1 楼层的第一个描述", "MT1 楼层的第二个描述"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中,`clip` 属性表示你的每张地图的那一部分会被当成数据集,例如填写 `[0, 0, 13, 13]` 就会让坐标为 `(0, 0)`,长宽为 `(13, 13)` 的矩形内容作为数据集。`special` 属性允许你针对单独的某几层设置不同的裁剪方式,例如设置 `MT11` 为 `[3, 3, 7, 7]` 等,如果没有设置默认使用 `defaults` 的裁剪方式。最好保证每个楼层大小一致,不然我还要手动分类。
|
||||
|
||||
`data` 是你的每一层的楼层描述,要求每一层都要有描述,每层描述可以有多个,要求可以准确或粗略描述楼层的一部分特征(不需要是全部,不然的话文字量会很大),不超过 64 个 token(每个中文字约 0.6 token,每个英文字母约 0.3 token),可以详细可以简略,推荐每层有三个描述以上,而且描述不要有过多的语义重复。
|
||||
|
||||
11. 在全塔属性中的楼层列表中去除不在数据集内的楼层
|
||||
12. 将 `project` 文件夹打包发给我即可
|
||||
|
||||
## 贡献 Minamo Model 数据集
|
||||
|
||||
首先需要对你的塔的地图进行处理,参考 [GINKA Model 数据处理方式](#贡献-ginka-model-数据集)的 1-9 和 11 步,同时最好每张地图至少有几个空格(没有也没有影响,只不过会导致这个地图参与训练的次数降低)
|
||||
|
||||
与 GINKA Model 类似,在 `project` 文件夹中添加 `minamo-config.json` 文件,打开后将如下模板粘贴进去:
|
||||
|
||||
```json
|
||||
{
|
||||
"clip": {
|
||||
"defaults": [0, 0, 13, 13],
|
||||
"special": {
|
||||
"MT11": [3, 3, 7, 7]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 `clip` 属性与 GINKA Model 的定义相同,参考上一小节即可。
|
||||
|
||||
将以上内容全部设置完毕后,将 `project` 文件夹打包发送给我,添加 `ginka-config.json` 时也可以作为 GINKA Model 训练集。
|
||||
|
||||
## 训练
|
||||
|
||||
如果你想自行训练模型,首先需要安装 `reqiurements.txt` 中所需的库,然后按照以下顺序操作:
|
||||
|
||||
1. 准备 Minamo Model 数据集,放置在根目录下,命名为 `minamo-dataset.json`
|
||||
2. 执行 `python -m minamo.train`,等待训练完毕
|
||||
3. 准备 GINKA Model 数据集,放置在根目录下,命名为 `ginka-dataset.json`
|
||||
4. 执行 `python -m ginka.train`,等待训练完毕
|
||||
5. 目录 `result/ginka.pth` 即为训练完毕的 GINKA 模型
|
||||
6. (可选)准备 Minamo Model / GINKA Model 验证集,放置在根目录下,命名为 `minamo-eval.json` / `ginka-eval.json`,训练时每 10 个 epoch 会进行一次验证推理,建议使用与训练集不同的塔数据作为验证集。
|
||||
|
||||
准备训练集和验证集时,可以使用命令行脚本自动处理。首先进入 `data` 文件夹,然后运行 `pnpm i` 安装所有依赖,然后执行下面的脚本:
|
||||
|
||||
```bash
|
||||
pnpm ginka "../ginka-dataset.json" "MyTower/project" # 通过 ginka-config.json 生成 GINKA 训练集
|
||||
pnpm ginka "../ginka-eval.json" "MyTower/project" # 通过 ginka-config.json 生成 GINKA 验证集
|
||||
pnpm minamo "../minamo-dataset.json" "MyTower/project" # 通过 minamo-config.json 生成 Minamo 训练集
|
||||
pnpm minamo "../minamo-eval.json" "MyTower/project" # 通过 minamo-config.json 生成 Minamo 验证集
|
||||
```
|
||||
25
data/package.json
Normal file
25
data/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "data",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"ginka": "tsx ./src/ginka.ts",
|
||||
"minamo": "tsx ./src/minamo.ts",
|
||||
"test:topo": "tsx ./src/topology/test.ts",
|
||||
"test:vision": "tsx ./src/vision/test.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^22.13.10",
|
||||
"tsx": "^4.19.3",
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^11.3.0"
|
||||
}
|
||||
}
|
||||
966
data/pnpm-lock.yaml
Normal file
966
data/pnpm-lock.yaml
Normal file
@ -0,0 +1,966 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
fs-extra:
|
||||
specifier: ^11.3.0
|
||||
version: 11.3.0
|
||||
devDependencies:
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
'@types/node':
|
||||
specifier: ^22.13.10
|
||||
version: 22.13.10
|
||||
tsx:
|
||||
specifier: ^4.19.3
|
||||
version: 4.19.3
|
||||
vitest:
|
||||
specifier: ^3.0.8
|
||||
version: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.1':
|
||||
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.1':
|
||||
resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.1':
|
||||
resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.1':
|
||||
resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.1':
|
||||
resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.1':
|
||||
resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.1':
|
||||
resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.1':
|
||||
resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.1':
|
||||
resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.1':
|
||||
resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.1':
|
||||
resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.1':
|
||||
resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.1':
|
||||
resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.1':
|
||||
resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.1':
|
||||
resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.1':
|
||||
resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.1':
|
||||
resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.1':
|
||||
resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.1':
|
||||
resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.35.0':
|
||||
resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.35.0':
|
||||
resolution: {integrity: sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.35.0':
|
||||
resolution: {integrity: sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.35.0':
|
||||
resolution: {integrity: sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.35.0':
|
||||
resolution: {integrity: sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.35.0':
|
||||
resolution: {integrity: sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.35.0':
|
||||
resolution: {integrity: sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.35.0':
|
||||
resolution: {integrity: sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.35.0':
|
||||
resolution: {integrity: sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.35.0':
|
||||
resolution: {integrity: sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.35.0':
|
||||
resolution: {integrity: sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.35.0':
|
||||
resolution: {integrity: sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.35.0':
|
||||
resolution: {integrity: sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.35.0':
|
||||
resolution: {integrity: sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
||||
|
||||
'@types/node@22.13.10':
|
||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||
|
||||
'@vitest/expect@3.0.8':
|
||||
resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==}
|
||||
|
||||
'@vitest/mocker@3.0.8':
|
||||
resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.0.8':
|
||||
resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==}
|
||||
|
||||
'@vitest/runner@3.0.8':
|
||||
resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==}
|
||||
|
||||
'@vitest/snapshot@3.0.8':
|
||||
resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==}
|
||||
|
||||
'@vitest/spy@3.0.8':
|
||||
resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==}
|
||||
|
||||
'@vitest/utils@3.0.8':
|
||||
resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
debug@4.4.0:
|
||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
es-module-lexer@1.6.0:
|
||||
resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==}
|
||||
|
||||
esbuild@0.25.1:
|
||||
resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
expect-type@1.2.0:
|
||||
resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
fs-extra@11.3.0:
|
||||
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@4.10.0:
|
||||
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
loupe@3.1.3:
|
||||
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nanoid@3.3.9:
|
||||
resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pathval@2.0.0:
|
||||
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.5.3:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
rollup@4.35.0:
|
||||
resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@3.8.1:
|
||||
resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinypool@1.0.2:
|
||||
resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@3.0.2:
|
||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tsx@4.19.3:
|
||||
resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
vite-node@3.0.8:
|
||||
resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite@6.2.2:
|
||||
resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
jiti: '>=1.21.0'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@3.0.8:
|
||||
resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/debug': ^4.1.12
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
'@vitest/browser': 3.0.8
|
||||
'@vitest/ui': 3.0.8
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/debug':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.1':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.35.0':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 22.13.10
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 22.13.10
|
||||
|
||||
'@types/node@22.13.10':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
||||
'@vitest/expect@3.0.8':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.8
|
||||
'@vitest/utils': 3.0.8
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.0.8(vite@6.2.2(@types/node@22.13.10)(tsx@4.19.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.8
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 6.2.2(@types/node@22.13.10)(tsx@4.19.3)
|
||||
|
||||
'@vitest/pretty-format@3.0.8':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/runner@3.0.8':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@3.0.8':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.0.8
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@3.0.8':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/utils@3.0.8':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.0.8
|
||||
loupe: 3.1.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
check-error: 2.1.1
|
||||
deep-eql: 5.0.2
|
||||
loupe: 3.1.3
|
||||
pathval: 2.0.0
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
debug@4.4.0:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
es-module-lexer@1.6.0: {}
|
||||
|
||||
esbuild@0.25.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.1
|
||||
'@esbuild/android-arm': 0.25.1
|
||||
'@esbuild/android-arm64': 0.25.1
|
||||
'@esbuild/android-x64': 0.25.1
|
||||
'@esbuild/darwin-arm64': 0.25.1
|
||||
'@esbuild/darwin-x64': 0.25.1
|
||||
'@esbuild/freebsd-arm64': 0.25.1
|
||||
'@esbuild/freebsd-x64': 0.25.1
|
||||
'@esbuild/linux-arm': 0.25.1
|
||||
'@esbuild/linux-arm64': 0.25.1
|
||||
'@esbuild/linux-ia32': 0.25.1
|
||||
'@esbuild/linux-loong64': 0.25.1
|
||||
'@esbuild/linux-mips64el': 0.25.1
|
||||
'@esbuild/linux-ppc64': 0.25.1
|
||||
'@esbuild/linux-riscv64': 0.25.1
|
||||
'@esbuild/linux-s390x': 0.25.1
|
||||
'@esbuild/linux-x64': 0.25.1
|
||||
'@esbuild/netbsd-arm64': 0.25.1
|
||||
'@esbuild/netbsd-x64': 0.25.1
|
||||
'@esbuild/openbsd-arm64': 0.25.1
|
||||
'@esbuild/openbsd-x64': 0.25.1
|
||||
'@esbuild/sunos-x64': 0.25.1
|
||||
'@esbuild/win32-arm64': 0.25.1
|
||||
'@esbuild/win32-ia32': 0.25.1
|
||||
'@esbuild/win32-x64': 0.25.1
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
|
||||
expect-type@1.2.0: {}
|
||||
|
||||
fs-extra@11.3.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@4.10.0:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
loupe@3.1.3: {}
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.9: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.5.3:
|
||||
dependencies:
|
||||
nanoid: 3.3.9
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
rollup@4.35.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.35.0
|
||||
'@rollup/rollup-android-arm64': 4.35.0
|
||||
'@rollup/rollup-darwin-arm64': 4.35.0
|
||||
'@rollup/rollup-darwin-x64': 4.35.0
|
||||
'@rollup/rollup-freebsd-arm64': 4.35.0
|
||||
'@rollup/rollup-freebsd-x64': 4.35.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.35.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.35.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.35.0
|
||||
'@rollup/rollup-linux-loongarch64-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.35.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.35.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.35.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.35.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.35.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@3.8.1: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinypool@1.0.2: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
tsx@4.19.3:
|
||||
dependencies:
|
||||
esbuild: 0.25.1
|
||||
get-tsconfig: 4.10.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
vite-node@3.0.8(@types/node@22.13.10)(tsx@4.19.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
es-module-lexer: 1.6.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.2.2(@types/node@22.13.10)(tsx@4.19.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@6.2.2(@types/node@22.13.10)(tsx@4.19.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.1
|
||||
postcss: 8.5.3
|
||||
rollup: 4.35.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.13.10
|
||||
fsevents: 2.3.3
|
||||
tsx: 4.19.3
|
||||
|
||||
vitest@3.0.8(@types/node@22.13.10)(tsx@4.19.3):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.0.8
|
||||
'@vitest/mocker': 3.0.8(vite@6.2.2(@types/node@22.13.10)(tsx@4.19.3))
|
||||
'@vitest/pretty-format': 3.0.8
|
||||
'@vitest/runner': 3.0.8
|
||||
'@vitest/snapshot': 3.0.8
|
||||
'@vitest/spy': 3.0.8
|
||||
'@vitest/utils': 3.0.8
|
||||
chai: 5.2.0
|
||||
debug: 4.4.0
|
||||
expect-type: 1.2.0
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
std-env: 3.8.1
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.2.2(@types/node@22.13.10)(tsx@4.19.3)
|
||||
vite-node: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.13.10
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
43
data/src/floor.ts
Normal file
43
data/src/floor.ts
Normal file
@ -0,0 +1,43 @@
|
||||
const numMap: Record<number, number> = {
|
||||
0: 0, // 空地
|
||||
1: 1, // 墙壁
|
||||
21: 2, // 钥匙
|
||||
27: 3, // 红宝石
|
||||
28: 4, // 蓝宝石
|
||||
31: 5, // 血瓶
|
||||
81: 6, // 门
|
||||
201: 7, // 弱怪
|
||||
202: 8, // 中怪
|
||||
203: 9, // 强怪
|
||||
87: 10, // 楼梯
|
||||
88: 10, // 楼梯
|
||||
161: 11, // 箭头
|
||||
162: 11, // 箭头
|
||||
163: 11, // 箭头
|
||||
164: 11 // 箭头
|
||||
};
|
||||
|
||||
export function convertFloor(
|
||||
map: number[][],
|
||||
[x, y, w, h]: [number, number, number, number],
|
||||
name: string,
|
||||
floorId: string
|
||||
) {
|
||||
const clipped: number[][] = [];
|
||||
|
||||
for (let nx = x; nx < x + w; nx++) {
|
||||
const row: number[] = [];
|
||||
for (let ny = y; ny < y + h; ny++) {
|
||||
const num = numMap[map[nx][ny]];
|
||||
if (num === void 0) {
|
||||
console.log(
|
||||
`⚠️ 魔塔 ${name} 的楼层 ${floorId} 中出现未知图块类型:${map[nx][ny]}`
|
||||
);
|
||||
}
|
||||
row.push(num ?? 0);
|
||||
}
|
||||
clipped.push(row);
|
||||
}
|
||||
|
||||
return clipped;
|
||||
}
|
||||
82
data/src/ginka.ts
Normal file
82
data/src/ginka.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { exists, writeFile } from 'fs-extra';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { convertFloor } from './floor';
|
||||
import { mergeDataset } from './utils';
|
||||
|
||||
interface GinkaConfig {
|
||||
clip: {
|
||||
defaults: [number, number, number, number];
|
||||
special: Record<string, [number, number, number, number]>;
|
||||
};
|
||||
data: Record<string, string[]>;
|
||||
}
|
||||
|
||||
interface GinkaTrainData {
|
||||
text: string[];
|
||||
map: number[][];
|
||||
size: [number, number];
|
||||
}
|
||||
|
||||
interface GinkaDataset {
|
||||
datasetId: number;
|
||||
data: Record<string, GinkaTrainData>;
|
||||
}
|
||||
|
||||
const [output, ...list] = process.argv.slice(2);
|
||||
|
||||
async function parseOneFloor(
|
||||
path: string,
|
||||
name: string,
|
||||
floorId: string,
|
||||
config: GinkaConfig
|
||||
): Promise<GinkaTrainData> {
|
||||
const floorFile = await readFile(path, 'utf-8');
|
||||
const floor: any = JSON.parse(floorFile.split('\n').slice(1).join('\n'));
|
||||
const map = floor.map as number[][];
|
||||
const clip = config.clip.special[floorId] ?? config.clip.defaults;
|
||||
|
||||
const clipped = convertFloor(map, clip, name, floorId);
|
||||
|
||||
if (!config.data[floorId]) {
|
||||
console.log(`⚠️ 魔塔 ${name} 的楼层 ${floorId} 不存在描述文本!`);
|
||||
}
|
||||
|
||||
const data: GinkaTrainData = {
|
||||
text: config.data[floorId],
|
||||
map: clipped,
|
||||
size: [clipped[0].length, clipped.length]
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function parseOne(path: string): Promise<GinkaDataset> {
|
||||
const dataFile = await readFile(join(path, 'data.js'), 'utf-8');
|
||||
const configFile = await readFile(join(path, 'ginka-config.json'), 'utf-8');
|
||||
const data: any = JSON.parse(dataFile.split('\n').slice(1).join('\n'));
|
||||
const config = JSON.parse(configFile) as GinkaConfig;
|
||||
const floorIds = data.main.floorIds as string[];
|
||||
const name = data.firstData.name as string;
|
||||
|
||||
const datas = await Promise.all(
|
||||
floorIds.map(v =>
|
||||
parseOneFloor(join(path, 'floors', `${v}.js`), name, v, config)
|
||||
)
|
||||
);
|
||||
|
||||
const dataset: GinkaDataset = {
|
||||
datasetId: Math.floor(Math.random() * 1e12),
|
||||
data: Object.fromEntries(datas.map((v, i) => [floorIds[i], v]))
|
||||
};
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const results = await Promise.all(list.map(v => parseOne(v)));
|
||||
const dataset = mergeDataset(...results);
|
||||
await writeFile(output, JSON.stringify(dataset, void 0), 'utf-8');
|
||||
const size = Object.keys(dataset.data).length;
|
||||
console.log(`✅ 已处理 ${list.length} 个塔,共 ${size} 个地图`);
|
||||
})();
|
||||
337
data/src/minamo.ts
Normal file
337
data/src/minamo.ts
Normal file
@ -0,0 +1,337 @@
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { convertFloor } from './floor';
|
||||
import { mergeDataset } from './utils';
|
||||
import { compareMap } from './topology/compare';
|
||||
import { mirrorMapX, mirrorMapY, rotateMap } from './topology/transform';
|
||||
import { directions, tileType } from './topology/graph';
|
||||
import { calculateVisualSimilarity } from './vision/similarity';
|
||||
|
||||
interface MinamoConfig {
|
||||
clip: {
|
||||
defaults: [number, number, number, number];
|
||||
special: Record<string, [number, number, number, number]>;
|
||||
};
|
||||
// data: Record<string, Record<string, number>>;
|
||||
}
|
||||
|
||||
interface MinamoTrainData {
|
||||
map1: number[][];
|
||||
map2: number[][];
|
||||
topoSimilarity: number;
|
||||
visionSimilarity: number;
|
||||
size: [number, number];
|
||||
}
|
||||
|
||||
interface MinamoDataset {
|
||||
datasetId: number;
|
||||
data: Record<string, MinamoTrainData>;
|
||||
}
|
||||
|
||||
const [output, ...list] = process.argv.slice(2);
|
||||
|
||||
function chooseFrom<T>(arr: T[], n: number): T[] {
|
||||
const copy = arr.slice();
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
let randIndex = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[randIndex]] = [copy[randIndex], copy[i]];
|
||||
}
|
||||
return copy.slice(0, n);
|
||||
}
|
||||
|
||||
function choosePair(n: number) {
|
||||
const totalCount = Math.round((n * (n - 1)) / 2);
|
||||
const count = Math.min(totalCount, 1000);
|
||||
const pairs: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
pairs.push(i * n + j);
|
||||
}
|
||||
}
|
||||
// 直接打乱后取前 count 个
|
||||
for (let i = pairs.length - 1; i > 0; i--) {
|
||||
let randIndex = Math.floor(Math.random() * (i + 1));
|
||||
[pairs[i], pairs[randIndex]] = [pairs[randIndex], pairs[i]];
|
||||
}
|
||||
|
||||
return pairs.slice(0, count);
|
||||
}
|
||||
|
||||
function transform(map: number[][], rot: number, flip: number) {
|
||||
let res = map;
|
||||
for (let i = 0; i < rot; i++) {
|
||||
res = rotateMap(res);
|
||||
}
|
||||
if (flip & 0b01) {
|
||||
res = mirrorMapX(res);
|
||||
}
|
||||
if (flip & 0b10) {
|
||||
res = mirrorMapY(res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function generateTransformData(
|
||||
id1: string,
|
||||
id2: string,
|
||||
map1: number[][],
|
||||
map2: number[][],
|
||||
simi: number
|
||||
) {
|
||||
const types: [rot: number, flip: number][] = [];
|
||||
for (const rot of [0, 1, 2, 3]) {
|
||||
for (const flip of [0b00, 0b01, 0b10, 0b11]) {
|
||||
if (rot === 0 && flip === 0) continue;
|
||||
types.push([rot, flip]);
|
||||
}
|
||||
}
|
||||
// 随机抽取最多两个
|
||||
const trans = chooseFrom(types, Math.floor(Math.random() * 3));
|
||||
return trans
|
||||
.map(([rot, flip]) => {
|
||||
const com1 = `${id1}.${rot}.${flip}:${id1}`;
|
||||
const com2 = `${id1}.${rot}.${flip}:${id2}`;
|
||||
const com3 = `${id2}.${rot}.${flip}:${id1}`;
|
||||
const com4 = `${id2}.${rot}.${flip}:${id2}`;
|
||||
const choose = chooseFrom(
|
||||
[com1, com2, com3, com4],
|
||||
Math.floor(Math.random() * 2)
|
||||
);
|
||||
const res: [id: string, data: MinamoTrainData][] = [];
|
||||
if (choose.includes(com1)) {
|
||||
const t = transform(map1, rot, flip);
|
||||
res.push([
|
||||
com1,
|
||||
{
|
||||
map1: t,
|
||||
map2: map1,
|
||||
topoSimilarity: 1,
|
||||
visionSimilarity: calculateVisualSimilarity(map1, t),
|
||||
size: [map1[0].length, map1.length]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (choose.includes(com2)) {
|
||||
const t = transform(map1, rot, flip);
|
||||
res.push([
|
||||
com2,
|
||||
{
|
||||
map1: t,
|
||||
map2: map2,
|
||||
topoSimilarity: simi,
|
||||
visionSimilarity: calculateVisualSimilarity(t, map2),
|
||||
size: [map1[0].length, map1.length]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (choose.includes(com3)) {
|
||||
const t = transform(map2, rot, flip);
|
||||
res.push([
|
||||
com3,
|
||||
{
|
||||
map1: t,
|
||||
map2: map1,
|
||||
topoSimilarity: simi,
|
||||
visionSimilarity: calculateVisualSimilarity(t, map1),
|
||||
size: [map1[0].length, map1.length]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (choose.includes(com4)) {
|
||||
const t = transform(map2, rot, flip);
|
||||
res.push([
|
||||
com4,
|
||||
{
|
||||
map1: t,
|
||||
map2: map2,
|
||||
topoSimilarity: 1,
|
||||
visionSimilarity: calculateVisualSimilarity(t, map2),
|
||||
size: [map1[0].length, map1.length]
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function generateSimilarData(id: string, map: number[][]) {
|
||||
// 生成最多五个微调地图
|
||||
const width = map[0].length;
|
||||
const height = map.length;
|
||||
const num = Math.floor(Math.random() * 6);
|
||||
const res: [id: string, data: MinamoTrainData][] = [];
|
||||
|
||||
for (let i = 0; i < num; i++) {
|
||||
const clone = map.map(v => v.slice());
|
||||
const prob = Math.random() * 0.3;
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
if (Math.random() > prob) {
|
||||
// 有一定的概率进行微调
|
||||
continue;
|
||||
}
|
||||
if (Math.random() < 0.2) {
|
||||
// 20% 概率与旁边图块互换位置
|
||||
const [dx, dy] =
|
||||
directions[
|
||||
Math.floor(Math.random() * directions.length)
|
||||
];
|
||||
const px = nx + dx;
|
||||
const py = ny + dy;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
[clone[ny][nx], clone[py][px]] = [
|
||||
clone[py][px],
|
||||
clone[ny][nx]
|
||||
];
|
||||
} else {
|
||||
// 80% 概率替换当前图块
|
||||
clone[ny][nx] = Math.floor(Math.random() * tileType.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
const id2 = `${id}.S${i}`;
|
||||
const sid = `${id}:${id2}`;
|
||||
const simi = compareMap(id, id2, map, clone);
|
||||
res.push([
|
||||
sid,
|
||||
{
|
||||
map1: map,
|
||||
map2: clone,
|
||||
size: [width, height],
|
||||
topoSimilarity: simi,
|
||||
visionSimilarity: calculateVisualSimilarity(map, clone)
|
||||
}
|
||||
]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function generateDataset(
|
||||
floors: Map<string, number[][]>,
|
||||
pairs: number[],
|
||||
floorIds: string[],
|
||||
config: MinamoConfig
|
||||
): Record<string, MinamoTrainData> {
|
||||
const data: Record<string, MinamoTrainData> = {};
|
||||
|
||||
pairs.forEach(v => {
|
||||
const num1 = Math.floor(v / floorIds.length);
|
||||
const num2 = v % floorIds.length;
|
||||
const id1 = floorIds[num1];
|
||||
const id2 = floorIds[num2];
|
||||
const map1 = floors.get(id1);
|
||||
const map2 = floors.get(id2);
|
||||
if (!map1 || !map2) return;
|
||||
const [w1, h1] = [map1[0].length, map1.length];
|
||||
const [w2, h2] = [map2[0].length, map2.length];
|
||||
if (w1 !== w2 || h1 !== h2) return;
|
||||
const topoSimilarity = compareMap(id1, id2, map1, map2);
|
||||
const visionSimilarity = calculateVisualSimilarity(map1, map2);
|
||||
const train: MinamoTrainData = {
|
||||
map1,
|
||||
map2,
|
||||
topoSimilarity,
|
||||
visionSimilarity,
|
||||
size: [w1, h1]
|
||||
};
|
||||
data[`${id1}:${id2}`] = train;
|
||||
// 自身与自身对比的训练集,保证模型对相同地图输出 1
|
||||
const self1 = `${id1}:${id1}`;
|
||||
const self2 = `${id2}:${id2}`;
|
||||
const selfTrain = chooseFrom(
|
||||
[self1, self2],
|
||||
Math.floor(Math.random() * 3)
|
||||
);
|
||||
if (selfTrain.includes(self1) && !data[`${id1}:${id1}`]) {
|
||||
const selfTrain1: MinamoTrainData = {
|
||||
map1: map1,
|
||||
map2: map1,
|
||||
topoSimilarity: 1,
|
||||
visionSimilarity: 1,
|
||||
size: [w1, h1]
|
||||
};
|
||||
data[`${id1}:${id1}`] = selfTrain1;
|
||||
}
|
||||
if (selfTrain.includes(self2) && !data[`${id2}:${id2}`]) {
|
||||
const selfTrain2: MinamoTrainData = {
|
||||
map1: map2,
|
||||
map2: map2,
|
||||
topoSimilarity: 1,
|
||||
visionSimilarity: 1,
|
||||
size: [w1, h1]
|
||||
};
|
||||
data[`${id2}:${id2}`] = selfTrain2;
|
||||
}
|
||||
// 翻转、旋转训练集
|
||||
Object.assign(
|
||||
data,
|
||||
Object.fromEntries(
|
||||
generateTransformData(id1, id2, map1, map2, topoSimilarity)
|
||||
)
|
||||
);
|
||||
// 地图微调训练集
|
||||
Object.assign(data, Object.fromEntries(generateSimilarData(id1, map1)));
|
||||
// Object.assign(data, Object.fromEntries(generateSimilarData(id2, map2)));
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function parseOne(path: string): Promise<MinamoDataset> {
|
||||
const dataFile = await readFile(join(path, 'data.js'), 'utf-8');
|
||||
const configFile = await readFile(
|
||||
join(path, 'minamo-config.json'),
|
||||
'utf-8'
|
||||
);
|
||||
const data: any = JSON.parse(dataFile.split('\n').slice(1).join('\n'));
|
||||
const config = JSON.parse(configFile) as MinamoConfig;
|
||||
const floorIds = data.main.floorIds as string[];
|
||||
const name = data.firstData.name as string;
|
||||
const length = floorIds.length;
|
||||
const totalCount = Math.round((length * (length - 1)) / 2);
|
||||
|
||||
const pairs = choosePair(length);
|
||||
|
||||
console.log(
|
||||
`✅ 在 ${name} 中发现 ${length} 个楼层,共 ${totalCount} 种组合,选取 ${pairs.length} 个组合`
|
||||
);
|
||||
|
||||
const floors = new Map(
|
||||
await Promise.all(
|
||||
floorIds.map<Promise<[string, number[][]]>>(async v => {
|
||||
const file = await readFile(
|
||||
join(path, 'floors', `${v}.js`),
|
||||
'utf-8'
|
||||
);
|
||||
const data = file.split('\n').slice(1).join('\n');
|
||||
const json = JSON.parse(data);
|
||||
const map = json.map;
|
||||
const clip = config.clip.special[v] ?? config.clip.defaults;
|
||||
// 裁剪
|
||||
const clipped = convertFloor(map, clip, name, v);
|
||||
return [v, clipped];
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const trainData = generateDataset(floors, pairs, floorIds, config);
|
||||
|
||||
const dataset: MinamoDataset = {
|
||||
datasetId: Math.floor(Math.random() * 1e12),
|
||||
data: trainData
|
||||
};
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const results = await Promise.all(list.map(v => parseOne(v)));
|
||||
const dataset = mergeDataset(...results);
|
||||
await writeFile(output, JSON.stringify(dataset, void 0), 'utf-8');
|
||||
const size = Object.keys(dataset.data).length;
|
||||
console.log(`✅ 已处理 ${list.length} 个塔,共 ${size} 个组合`);
|
||||
})();
|
||||
29
data/src/topology/compare.ts
Normal file
29
data/src/topology/compare.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { buildTopologicalGraph } from './graph';
|
||||
import { GinkaTopologicalGraphs } from './interface';
|
||||
import { overallSimilarity } from './similarity';
|
||||
|
||||
const cache = new Map<string, GinkaTopologicalGraphs>();
|
||||
|
||||
export function getTopologicalGraph(
|
||||
floorId: string,
|
||||
map: number[][]
|
||||
): GinkaTopologicalGraphs {
|
||||
if (cache.has(floorId)) return cache.get(floorId)!;
|
||||
const graphs = buildTopologicalGraph(map);
|
||||
cache.set(floorId, graphs);
|
||||
return graphs;
|
||||
}
|
||||
|
||||
export function compareMap(
|
||||
floorId1: string,
|
||||
floorId2: string,
|
||||
map1: number[][],
|
||||
map2: number[][]
|
||||
) {
|
||||
const graph1 = getTopologicalGraph(floorId1, map1);
|
||||
const graph2 = getTopologicalGraph(floorId2, map2);
|
||||
|
||||
const kernel = overallSimilarity(graph1, graph2);
|
||||
|
||||
return kernel;
|
||||
}
|
||||
254
data/src/topology/graph.ts
Normal file
254
data/src/topology/graph.ts
Normal file
@ -0,0 +1,254 @@
|
||||
import {
|
||||
ResourceArea,
|
||||
GinkaGraph,
|
||||
BranchNode,
|
||||
GinkaTopologicalGraphs,
|
||||
ResourceNode
|
||||
} from './interface';
|
||||
|
||||
export const tileType = new Set(
|
||||
Array(12)
|
||||
.fill(0)
|
||||
.map((_, i) => i)
|
||||
);
|
||||
const branchType = new Set([6, 7, 8, 9]);
|
||||
const entranceType = new Set([10, 11]);
|
||||
const resourceType = new Set([0, 2, 3, 4, 5, 10, 11]);
|
||||
|
||||
export const directions: [number, number][] = [
|
||||
[-1, 0],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[0, 1]
|
||||
];
|
||||
|
||||
function buildGraphFromEntrance(
|
||||
map: number[][],
|
||||
entrance: number,
|
||||
resourceMap: Map<number, number>,
|
||||
areaMap: ResourceArea[]
|
||||
): GinkaGraph {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
const visitedEntrance = new Set<number>([entrance]);
|
||||
const visited = new Set<number>();
|
||||
const queue: [number, number][] = [];
|
||||
queue.push([entrance % width, Math.floor(entrance / width)]);
|
||||
|
||||
const branchNodes = new Set<number>();
|
||||
|
||||
// 1. BFS 检测所有分支节点
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) continue;
|
||||
const [nx, ny] = item;
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index)) continue;
|
||||
const tile = map[ny][nx];
|
||||
|
||||
if (entranceType.has(tile)) {
|
||||
visitedEntrance.add(index);
|
||||
}
|
||||
if (branchType.has(tile)) {
|
||||
branchNodes.add(index);
|
||||
}
|
||||
visited.add(index);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = dx + nx;
|
||||
const py = dy + ny;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
const tile = map[py][px];
|
||||
if (tile !== 1) {
|
||||
// 非墙区域可通行
|
||||
queue.push([px, py]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从分支节点构建拓扑图
|
||||
const graph = new Map<number, BranchNode | ResourceNode>();
|
||||
branchNodes.forEach(v => {
|
||||
const nx = v % width;
|
||||
const ny = Math.floor(v / width);
|
||||
if (!graph.get(v)) {
|
||||
graph.set(v, {
|
||||
type: 'branch',
|
||||
neighbor: new Set(),
|
||||
tile: map[ny][nx]
|
||||
});
|
||||
}
|
||||
const node = graph.get(v)!;
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = nx + dx;
|
||||
const py = ny + dy;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
const index = py * width + px;
|
||||
|
||||
// 先检查临近节点是不是分支节点,是的话链接到自己
|
||||
if (branchNodes.has(index)) {
|
||||
node.neighbor.add(index);
|
||||
} else {
|
||||
// 检查是不是资源节点
|
||||
const pointer = resourceMap.get(index);
|
||||
if (pointer === void 0) continue;
|
||||
const area = areaMap[pointer];
|
||||
if (!area) continue;
|
||||
area.neighbor.add(v);
|
||||
area.members.forEach(v => {
|
||||
node.neighbor.add(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 把资源节点拆分成并排,并放入拓扑图
|
||||
areaMap.forEach(v => {
|
||||
v.members.forEach(index => {
|
||||
const nx = index % width;
|
||||
const ny = Math.floor(index / width);
|
||||
const tile = map[ny][nx];
|
||||
if (tile === 0) return;
|
||||
const node: ResourceNode = {
|
||||
type: 'resource',
|
||||
resourceType: tile,
|
||||
neighbor: v.neighbor,
|
||||
resourceArea: v
|
||||
};
|
||||
graph.set(index, node);
|
||||
});
|
||||
});
|
||||
|
||||
return { graph, resourceMap, areaMap, visitedEntrance, visited };
|
||||
}
|
||||
|
||||
function findResourceNodes(map: number[][]) {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
const visited = new Set<number>();
|
||||
const areas: ResourceArea[] = [];
|
||||
const resourcesMap: Map<number, number> = new Map();
|
||||
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const tile = map[ny][nx];
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index) || !resourceType.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
const queue: [number, number][] = [];
|
||||
queue.push([nx, ny]);
|
||||
const area: ResourceArea = {
|
||||
type: 'resource',
|
||||
resources: new Map([[tile, 1]]),
|
||||
members: new Set([index]),
|
||||
neighbor: new Set()
|
||||
};
|
||||
|
||||
while (queue.length > 0) {
|
||||
const item = queue.shift();
|
||||
if (!item) continue;
|
||||
const [nx, ny] = item;
|
||||
const index = ny * width + nx;
|
||||
if (visited.has(index)) {
|
||||
continue;
|
||||
}
|
||||
const tile = map[ny][nx];
|
||||
if (!resourceType.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(index);
|
||||
|
||||
const exists = area.resources.get(tile);
|
||||
if (!exists) {
|
||||
area.resources.set(tile, 1);
|
||||
} else {
|
||||
area.resources.set(tile, exists + 1);
|
||||
}
|
||||
area.members.add(index);
|
||||
resourcesMap.set(index, areas.length);
|
||||
|
||||
for (const [dx, dy] of directions) {
|
||||
const px = nx + dx;
|
||||
const py = ny + dy;
|
||||
if (px < 0 || px >= width || py < 0 || py >= height) {
|
||||
continue;
|
||||
}
|
||||
queue.push([px, py]);
|
||||
}
|
||||
}
|
||||
|
||||
areas.push(area);
|
||||
}
|
||||
}
|
||||
|
||||
return { areaMap: areas, resourcesMap };
|
||||
}
|
||||
|
||||
export function buildTopologicalGraph(map: number[][]): GinkaTopologicalGraphs {
|
||||
const width = map[0].length;
|
||||
const height = map[1].length;
|
||||
|
||||
// 1. 找到所有入口
|
||||
const entrances = new Set<number>();
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const tile = map[ny][nx];
|
||||
if (entranceType.has(tile)) {
|
||||
entrances.add(ny * width + nx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 找到所有的资源节点
|
||||
const { areaMap, resourcesMap } = findResourceNodes(map);
|
||||
|
||||
// 3. 对每个入口计算拓扑图
|
||||
const graphs: GinkaGraph[] = [];
|
||||
const usedEntrance = new Set<number>();
|
||||
const totalVisited = new Set<number>();
|
||||
/** 入口位置到拓扑图的映射 */
|
||||
const entranceMap = new Map<number, GinkaGraph>();
|
||||
entrances.forEach(v => {
|
||||
if (usedEntrance.has(v)) {
|
||||
return;
|
||||
}
|
||||
const nx = v % width;
|
||||
const ny = Math.floor(v / width);
|
||||
const entranceGraph = buildGraphFromEntrance(
|
||||
map,
|
||||
v,
|
||||
resourcesMap,
|
||||
areaMap
|
||||
);
|
||||
const { graph, visited, visitedEntrance } = entranceGraph;
|
||||
graphs.push(entranceGraph);
|
||||
// 标记已经探索到的入口,并标记这个入口对应了哪个图
|
||||
visitedEntrance.forEach(v => {
|
||||
usedEntrance.add(v);
|
||||
entranceMap.set(v, entranceGraph);
|
||||
});
|
||||
visited.forEach(v => {
|
||||
totalVisited.add(v);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 计算不可到达区域
|
||||
const unreachable = new Set<number>();
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
for (let nx = 0; nx < width; nx++) {
|
||||
const index = ny * width + nx;
|
||||
if (!totalVisited.has(index) && map[ny][nx] !== 1) {
|
||||
unreachable.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { graphs, entranceMap, unreachable };
|
||||
}
|
||||
43
data/src/topology/interface.ts
Normal file
43
data/src/topology/interface.ts
Normal file
@ -0,0 +1,43 @@
|
||||
export interface ResourceArea {
|
||||
type: 'resource';
|
||||
resources: Map<number, number>;
|
||||
members: Set<number>;
|
||||
neighbor: Set<number>;
|
||||
}
|
||||
|
||||
export interface BranchNode {
|
||||
type: 'branch';
|
||||
neighbor: Set<number>;
|
||||
tile: number;
|
||||
}
|
||||
|
||||
export interface ResourceNode {
|
||||
type: 'resource';
|
||||
resourceType: number;
|
||||
neighbor: Set<number>;
|
||||
resourceArea: ResourceArea;
|
||||
}
|
||||
|
||||
export type GinkaNode = BranchNode | ResourceNode;
|
||||
|
||||
export interface GinkaGraph {
|
||||
/** 拓扑图内容,键表示位置,值表示这一点的节点 */
|
||||
graph: Map<number, GinkaNode>;
|
||||
/** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */
|
||||
resourceMap: Map<number, number>;
|
||||
/** 资源区域列表 */
|
||||
areaMap: ResourceArea[];
|
||||
/** 这个拓扑图包含的入口位置 */
|
||||
visitedEntrance: Set<number>;
|
||||
/** 这个拓扑图能够造访的所有位置 */
|
||||
visited: Set<number>;
|
||||
}
|
||||
|
||||
export interface GinkaTopologicalGraphs {
|
||||
/** 这个地图包含的所有独立的图 */
|
||||
graphs: GinkaGraph[];
|
||||
/** 每个入口对应哪个图 */
|
||||
entranceMap: Map<number, GinkaGraph>;
|
||||
/** 这个图从入口开始的不可到达区域 */
|
||||
unreachable: Set<number>;
|
||||
}
|
||||
171
data/src/topology/similarity.ts
Normal file
171
data/src/topology/similarity.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { cosineSimilarity } from 'src/utils';
|
||||
import { GinkaGraph, GinkaTopologicalGraphs } from './interface';
|
||||
|
||||
interface WLNode {
|
||||
originalPos: number;
|
||||
originalLabel: string;
|
||||
currentLabel: string;
|
||||
neighbors: WLNode[];
|
||||
}
|
||||
|
||||
function encodeNodeLabels(graph: GinkaGraph) {
|
||||
const nodes: WLNode[] = [];
|
||||
const nodeMap = new Map<number, WLNode>();
|
||||
|
||||
graph.graph.forEach((node, pos) => {
|
||||
let label: string;
|
||||
|
||||
// 编码为唯一哈希值(用字符串就行,V8 会自动帮你算哈希)
|
||||
if (node.type === 'branch') {
|
||||
label = `B:${node.tile}`;
|
||||
} else {
|
||||
label = `R:${node.resourceType}`;
|
||||
}
|
||||
|
||||
const wlNode: WLNode = {
|
||||
originalPos: pos,
|
||||
originalLabel: label,
|
||||
currentLabel: label,
|
||||
neighbors: []
|
||||
};
|
||||
nodeMap.set(pos, wlNode);
|
||||
nodes.push(wlNode);
|
||||
});
|
||||
|
||||
// 映射邻居节点
|
||||
nodes.forEach(node => {
|
||||
const ginkaNode = graph.graph.get(node.originalPos);
|
||||
ginkaNode?.neighbor.forEach(v => {
|
||||
const wl = nodeMap.get(v);
|
||||
if (wl) node.neighbors.push(wl);
|
||||
});
|
||||
});
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function weisfeilerLehmanIteration(
|
||||
nodes: WLNode[],
|
||||
iterations: number,
|
||||
decay: number = 0.6 // 衰减权重,减小长距离图的权重
|
||||
) {
|
||||
const labelHistory: string[][] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const newLabels: string[] = [];
|
||||
|
||||
// 生成新标签
|
||||
nodes.forEach(node => {
|
||||
const neighborLabels = node.neighbors
|
||||
.map(n => n.currentLabel)
|
||||
.sort();
|
||||
const compositeLabel = `${node.currentLabel}|${neighborLabels.join(
|
||||
','
|
||||
)}`;
|
||||
|
||||
newLabels.push(compositeLabel);
|
||||
});
|
||||
|
||||
// 更新节点标签并记录
|
||||
nodes.forEach((node, idx) => {
|
||||
node.currentLabel = newLabels[idx];
|
||||
});
|
||||
labelHistory.push([...newLabels]);
|
||||
}
|
||||
|
||||
// 统计每个节点的数量
|
||||
let weight = 1;
|
||||
const numMap = new Map<string, number>();
|
||||
labelHistory.forEach(iter => {
|
||||
iter.forEach(v => {
|
||||
if (!numMap.has(v)) {
|
||||
numMap.set(v, weight);
|
||||
} else {
|
||||
numMap.set(v, numMap.get(v)! + weight);
|
||||
}
|
||||
});
|
||||
weight *= decay;
|
||||
});
|
||||
// 把每个节点的原始标签也加上,权重使用最远权重,可以认为是资源重复率
|
||||
nodes.forEach(node => {
|
||||
if (!numMap.has(node.originalLabel)) {
|
||||
numMap.set(node.originalLabel, weight);
|
||||
} else {
|
||||
numMap.set(
|
||||
node.originalLabel,
|
||||
numMap.get(node.originalLabel)! + weight
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return numMap;
|
||||
}
|
||||
|
||||
function vectorizeFeatures(features: Map<string, number>, vocab: string[]) {
|
||||
const vec: number[] = new Array(vocab.length).fill(0);
|
||||
|
||||
features.forEach((count, label) => {
|
||||
const index = vocab.indexOf(label);
|
||||
if (index !== -1) {
|
||||
vec[index] += count;
|
||||
}
|
||||
});
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
function wlKernel(
|
||||
graphA: GinkaGraph,
|
||||
graphB: GinkaGraph,
|
||||
iterations = 3
|
||||
): number {
|
||||
// 编码节点
|
||||
const nodesA = encodeNodeLabels(graphA);
|
||||
const nodesB = encodeNodeLabels(graphB);
|
||||
|
||||
// 迭代生成标签
|
||||
const featuresA = weisfeilerLehmanIteration(nodesA, iterations);
|
||||
const featuresB = weisfeilerLehmanIteration(nodesB, iterations);
|
||||
|
||||
// 构建特征向量
|
||||
const vocab = [...new Set([...featuresA.keys(), ...featuresB.keys()])];
|
||||
const vecA = vectorizeFeatures(featuresA, vocab);
|
||||
const vecB = vectorizeFeatures(featuresB, vocab);
|
||||
|
||||
// 计算余弦相似度
|
||||
return cosineSimilarity(vecA, vecB);
|
||||
}
|
||||
|
||||
export function overallSimilarity(
|
||||
a: GinkaTopologicalGraphs,
|
||||
b: GinkaTopologicalGraphs
|
||||
) {
|
||||
// 使用 Weisfeiler-Lehman Kernel 方式计算拓扑图相似度
|
||||
const graphsA = a.graphs;
|
||||
const graphsB = b.graphs;
|
||||
|
||||
let totalSimilarity = 0;
|
||||
const comparedGraph = new Set<GinkaGraph>();
|
||||
graphsA.forEach(ga => {
|
||||
let maxSimilarity = 0;
|
||||
let maxGraph: GinkaGraph | null = null;
|
||||
// 图之间两两比较,找到最接近的作为相似度
|
||||
for (const gb of graphsB) {
|
||||
if (comparedGraph.has(gb)) continue;
|
||||
// 计算迭代次数
|
||||
const min = Math.min(ga.graph.size, gb.graph.size);
|
||||
const iterations = Math.ceil(Math.max(1, Math.log(min)));
|
||||
const similarity = wlKernel(ga, gb, iterations);
|
||||
if (similarity > maxSimilarity) {
|
||||
maxSimilarity = similarity;
|
||||
maxGraph = gb;
|
||||
}
|
||||
if (similarity === 1) break;
|
||||
}
|
||||
totalSimilarity += maxSimilarity;
|
||||
if (maxGraph) comparedGraph.add(maxGraph);
|
||||
});
|
||||
|
||||
// 取根号使结果更接近线性
|
||||
return Math.sqrt(totalSimilarity / graphsA.length);
|
||||
}
|
||||
95
data/src/topology/test.ts
Normal file
95
data/src/topology/test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { buildTopologicalGraph } from './graph';
|
||||
import { mirrorMapX, mirrorMapY, rotateMap } from './transform';
|
||||
import { overallSimilarity } from './similarity';
|
||||
|
||||
(() => {
|
||||
// MT3
|
||||
const map1 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 10, 7, 3, 1],
|
||||
[1, 6, 1, 5, 1, 7, 1],
|
||||
[1, 9, 1, 8, 1, 6, 1],
|
||||
[1, 5, 8, 0, 1, 7, 1],
|
||||
[1, 2, 1, 5, 7, 10, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT6
|
||||
const map2 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 6, 4, 1, 10, 1],
|
||||
[1, 6, 1, 9, 1, 5, 1],
|
||||
[1, 8, 0, 6, 0, 8, 1],
|
||||
[1, 5, 1, 10, 1, 2, 1],
|
||||
[1, 9, 3, 1, 4, 9, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT8
|
||||
// const map2 = [
|
||||
// [1, 1, 1, 1, 1, 1, 1],
|
||||
// [1, 5, 8, 10, 7, 2, 1],
|
||||
// [1, 2, 1, 5, 1, 7, 1],
|
||||
// [1, 3, 1, 3, 6, 4, 1],
|
||||
// [1, 6, 1, 6, 1, 8, 1],
|
||||
// [1, 10, 7, 5, 1, 5, 1],
|
||||
// [1, 1, 1, 1, 1, 1, 1]
|
||||
// ];
|
||||
// MT3 微调
|
||||
// const map2 = [
|
||||
// [1, 1, 1, 1, 1, 1, 1],
|
||||
// [1, 3, 1, 10, 7, 3, 1],
|
||||
// [1, 6, 1, 5, 1, 7, 1],
|
||||
// [1, 9, 1, 8, 1, 6, 1],
|
||||
// [1, 5, 8, 0, 1, 7, 1],
|
||||
// [1, 2, 1, 4, 7, 10, 1],
|
||||
// [1, 1, 1, 1, 1, 1, 1]
|
||||
// ];
|
||||
|
||||
// 1. 两张图与自身对比
|
||||
const graph1 = buildTopologicalGraph(map1);
|
||||
const graph2 = buildTopologicalGraph(map2);
|
||||
|
||||
console.log(`map1 vs map1: ${overallSimilarity(graph1, graph1)}`);
|
||||
console.log(`map2 vs map2: ${overallSimilarity(graph2, graph2)}`);
|
||||
|
||||
// 2. 两张图相互对比
|
||||
console.log(`map1 vs map2: ${overallSimilarity(graph1, graph2)}`);
|
||||
console.log(`map2 vs map1: ${overallSimilarity(graph2, graph1)}`);
|
||||
|
||||
// 3. x镜像对比
|
||||
const xFlipped1 = mirrorMapX(map1);
|
||||
const xFlipped2 = mirrorMapX(map2);
|
||||
const graphX1 = buildTopologicalGraph(xFlipped1);
|
||||
const graphX2 = buildTopologicalGraph(xFlipped2);
|
||||
console.log(`map1:x vs map1: ${overallSimilarity(graphX1, graph1)}`);
|
||||
console.log(`map1:x vs map2: ${overallSimilarity(graphX1, graph2)}`);
|
||||
console.log(`map1 vs map2:x: ${overallSimilarity(graph1, graphX2)}`);
|
||||
console.log(`map2:x vs map2: ${overallSimilarity(graphX2, graph2)}`);
|
||||
console.log(`map2:x vs map1: ${overallSimilarity(graphX2, graph2)}`);
|
||||
console.log(`map2 vs map1:x: ${overallSimilarity(graph2, graphX1)}`);
|
||||
|
||||
// 4. y镜像对比
|
||||
const yFlipped1 = mirrorMapY(map1);
|
||||
const yFlipped2 = mirrorMapY(map2);
|
||||
const graphY1 = buildTopologicalGraph(yFlipped1);
|
||||
const graphY2 = buildTopologicalGraph(yFlipped2);
|
||||
console.log(`map1:y vs map1: ${overallSimilarity(graphY1, graph1)}`);
|
||||
console.log(`map1:y vs map2: ${overallSimilarity(graphY1, graph2)}`);
|
||||
console.log(`map1 vs map2:y: ${overallSimilarity(graph1, graphY2)}`);
|
||||
console.log(`map2:y vs map2: ${overallSimilarity(graphY2, graph2)}`);
|
||||
console.log(`map2:y vs map1: ${overallSimilarity(graphY2, graph1)}`);
|
||||
console.log(`map2 vs map1:y: ${overallSimilarity(graph2, graphY1)}`);
|
||||
|
||||
// 5. xy 镜像混合对比
|
||||
console.log(`map1:x vs map1:y: ${overallSimilarity(graphX1, graphY1)}`);
|
||||
console.log(`map1:y vs map2:x: ${overallSimilarity(graphY1, graphX2)}`);
|
||||
console.log(`map1:x vs map2:x: ${overallSimilarity(graphX1, graphX2)}`);
|
||||
console.log(`map1:x vs map2:y: ${overallSimilarity(graphX1, graphY2)}`);
|
||||
|
||||
// 6. 旋转对比
|
||||
const rot901 = rotateMap(map1);
|
||||
const rot902 = rotateMap(map2);
|
||||
const graph901 = buildTopologicalGraph(rot901);
|
||||
const graph902 = buildTopologicalGraph(rot902);
|
||||
console.log(`map1:90 vs map1: ${overallSimilarity(graph1, graph901)}`);
|
||||
console.log(`map2:90 vs map2: ${overallSimilarity(graph2, graph902)}`);
|
||||
})();
|
||||
13
data/src/topology/transform.ts
Normal file
13
data/src/topology/transform.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function mirrorMapX(map: number[][]) {
|
||||
return map.map(v => [...v].reverse());
|
||||
}
|
||||
|
||||
export function mirrorMapY(map: number[][]) {
|
||||
return [...map].reverse();
|
||||
}
|
||||
|
||||
export function rotateMap(map: number[][]) {
|
||||
return [
|
||||
...map[0].map((_, colIndex) => map.map(row => row[colIndex]))
|
||||
].reverse();
|
||||
}
|
||||
40
data/src/utils.ts
Normal file
40
data/src/utils.ts
Normal file
@ -0,0 +1,40 @@
|
||||
interface DatasetMergable<T> {
|
||||
datasetId: number;
|
||||
data: Record<string, T>;
|
||||
}
|
||||
|
||||
export function mergeDataset<T>(
|
||||
...datasets: DatasetMergable<T>[]
|
||||
): DatasetMergable<T> {
|
||||
const data: Record<string, T> = {};
|
||||
datasets.forEach(v => {
|
||||
for (const [key, value] of Object.entries(v.data)) {
|
||||
const dataKey = `${v.datasetId}/${key}`;
|
||||
data[dataKey] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const dataset: DatasetMergable<T> = {
|
||||
datasetId: Math.floor(Math.random() * 1e12),
|
||||
data: data
|
||||
};
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
export function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
||||
if (vecA.length !== vecB.length) {
|
||||
throw new Error('Vectors must have same dimension');
|
||||
}
|
||||
|
||||
let dot = 0,
|
||||
normA = 0,
|
||||
normB = 0;
|
||||
for (let i = 0; i < vecA.length; i++) {
|
||||
dot += vecA[i] * vecB[i];
|
||||
normA += vecA[i] ** 2;
|
||||
normB += vecB[i] ** 2;
|
||||
}
|
||||
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
144
data/src/vision/similarity.ts
Normal file
144
data/src/vision/similarity.ts
Normal file
@ -0,0 +1,144 @@
|
||||
interface VisualSimilarityConfig {
|
||||
// 类型重要性权重(需根据游戏设定调整)
|
||||
typeWeights: { [key: number]: number };
|
||||
// 是否启用视觉焦点增强
|
||||
enableVisualFocus: boolean;
|
||||
// 是否启用密度感知
|
||||
enableDensityAwareness: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: VisualSimilarityConfig = {
|
||||
typeWeights: {
|
||||
0: 0.2, // 空地
|
||||
1: 0.3, // 墙壁
|
||||
2: 0.6, // 钥匙
|
||||
3: 0.7, // 红宝石
|
||||
4: 0.7, // 蓝宝石
|
||||
5: 0.5, // 血瓶
|
||||
6: 0.4, // 门
|
||||
7: 0.5, // 弱怪
|
||||
8: 0.6, // 中怪
|
||||
9: 0.6, // 强怪
|
||||
10: 0.4, // 楼梯
|
||||
11: 0.4 // 箭头
|
||||
},
|
||||
enableVisualFocus: true,
|
||||
enableDensityAwareness: true
|
||||
};
|
||||
|
||||
export function calculateVisualSimilarity(
|
||||
map1: number[][],
|
||||
map2: number[][],
|
||||
config = DEFAULT_CONFIG
|
||||
): number {
|
||||
// 尺寸校验
|
||||
if (map1.length !== map2.length || map1[0]?.length !== map2[0]?.length) {
|
||||
return 0; // 或抛出异常
|
||||
}
|
||||
|
||||
const rows = map1.length;
|
||||
const cols = map1[0].length;
|
||||
let totalScore = 0;
|
||||
let maxPossibleScore = 0;
|
||||
|
||||
// 视觉焦点权重图
|
||||
const focusWeights = config.enableVisualFocus
|
||||
? generateFocusWeights(rows, cols)
|
||||
: Array(rows)
|
||||
.fill(1)
|
||||
.map(() => Array(cols).fill(1));
|
||||
|
||||
// 类型密度分布计算
|
||||
const densityMap = config.enableDensityAwareness
|
||||
? calculateDensityImpact(map1, map2, config.typeWeights)
|
||||
: Array(rows)
|
||||
.fill(1)
|
||||
.map(() => Array(cols).fill(1));
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
const type1 = map1[i][j];
|
||||
const type2 = map2[i][j];
|
||||
|
||||
// 基础类型权重
|
||||
const baseWeight = Math.max(
|
||||
config.typeWeights[type1] || 0.5,
|
||||
config.typeWeights[type2] || 0.5
|
||||
);
|
||||
|
||||
// 空间权重组合
|
||||
const spatialWeight = focusWeights[i][j] * densityMap[i][j];
|
||||
|
||||
// 类型匹配得分
|
||||
const typeScore = type1 === type2 ? 1 : 0;
|
||||
|
||||
totalScore += typeScore * baseWeight * spatialWeight;
|
||||
maxPossibleScore += baseWeight * spatialWeight;
|
||||
}
|
||||
}
|
||||
|
||||
return maxPossibleScore > 0 ? totalScore / maxPossibleScore : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成视觉焦点权重图(基于人类视觉注意力分布)
|
||||
*/
|
||||
function generateFocusWeights(rows: number, cols: number): number[][] {
|
||||
const weights = [];
|
||||
const centerX = cols / 2;
|
||||
const centerY = rows / 2;
|
||||
const maxDist = Math.sqrt(centerX ** 2 + centerY ** 2) * 0.7;
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const rowWeights = [];
|
||||
for (let j = 0; j < cols; j++) {
|
||||
// 使用高斯分布模拟视觉焦点
|
||||
const dx = (j - centerX) / cols;
|
||||
const dy = (i - centerY) / rows;
|
||||
const distance = Math.sqrt(dx ** 2 + dy ** 2);
|
||||
const gaussian = Math.exp(-(distance ** 2) / (2 * 0.3 ** 2));
|
||||
rowWeights.push(1.0 + 0.6 * gaussian); // 中心区域最高1.6倍权重
|
||||
}
|
||||
weights.push(rowWeights);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算类型密度影响权重
|
||||
*/
|
||||
function calculateDensityImpact(
|
||||
map1: number[][],
|
||||
map2: number[][],
|
||||
typeWeights: { [key: number]: number }
|
||||
): number[][] {
|
||||
const rows = map1.length;
|
||||
const cols = map1[0].length;
|
||||
const densityMap = Array(rows)
|
||||
.fill(0)
|
||||
.map(() => Array(cols).fill(0));
|
||||
|
||||
// 滑动窗口分析局部密度
|
||||
const windowSize = 3;
|
||||
const halfWindow = Math.floor(windowSize / 2);
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let j = 0; j < cols; j++) {
|
||||
let density = 0;
|
||||
for (let di = -halfWindow; di <= halfWindow; di++) {
|
||||
for (let dj = -halfWindow; dj <= halfWindow; dj++) {
|
||||
const ni = i + di;
|
||||
const nj = j + dj;
|
||||
if (ni >= 0 && ni < rows && nj >= 0 && nj < cols) {
|
||||
const weight1 = typeWeights[map1[ni][nj]] || 0.5;
|
||||
const weight2 = typeWeights[map2[ni][nj]] || 0.5;
|
||||
density += (weight1 + weight2) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 密度权重:高密度区域增强对比度
|
||||
densityMap[i][j] = 1.0 + 0.4 * (density / windowSize ** 2);
|
||||
}
|
||||
}
|
||||
return densityMap;
|
||||
}
|
||||
74
data/src/vision/test.ts
Normal file
74
data/src/vision/test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { calculateVisualSimilarity } from './similarity';
|
||||
|
||||
(() => {
|
||||
// MT3
|
||||
const map1 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 10, 7, 3, 1],
|
||||
[1, 6, 1, 5, 1, 7, 1],
|
||||
[1, 9, 1, 8, 1, 6, 1],
|
||||
[1, 5, 8, 0, 1, 7, 1],
|
||||
[1, 2, 1, 5, 7, 10, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT6
|
||||
const map2 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 6, 4, 1, 10, 1],
|
||||
[1, 6, 1, 9, 1, 5, 1],
|
||||
[1, 8, 0, 6, 0, 8, 1],
|
||||
[1, 5, 1, 10, 1, 2, 1],
|
||||
[1, 9, 3, 1, 4, 9, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT8
|
||||
const map3 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 8, 10, 7, 2, 1],
|
||||
[1, 2, 1, 5, 1, 7, 1],
|
||||
[1, 3, 1, 3, 6, 4, 1],
|
||||
[1, 6, 1, 6, 1, 8, 1],
|
||||
[1, 10, 7, 5, 1, 5, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT3 微调
|
||||
const map4 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 10, 7, 3, 1],
|
||||
[1, 6, 1, 5, 1, 7, 1],
|
||||
[1, 9, 1, 8, 1, 6, 1],
|
||||
[1, 5, 8, 0, 1, 7, 1],
|
||||
[1, 2, 1, 4, 7, 10, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
// MT10
|
||||
const map5 = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 1, 10, 1, 5, 1],
|
||||
[1, 6, 7, 7, 7, 6, 1],
|
||||
[1, 1, 6, 5, 6, 1, 1],
|
||||
[1, 4, 5, 9, 5, 4, 1],
|
||||
[1, 3, 1, 1, 1, 3, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
];
|
||||
|
||||
// 测试自我对比
|
||||
console.log(`map1 vs map1: ${calculateVisualSimilarity(map1, map1)}`);
|
||||
console.log(`map2 vs map2: ${calculateVisualSimilarity(map2, map2)}`);
|
||||
console.log(`map3 vs map3: ${calculateVisualSimilarity(map3, map3)}`);
|
||||
console.log(`map4 vs map4: ${calculateVisualSimilarity(map4, map4)}`);
|
||||
// 两两测试
|
||||
console.log(`map1 vs map2: ${calculateVisualSimilarity(map1, map2)}`);
|
||||
console.log(`map1 vs map3: ${calculateVisualSimilarity(map1, map3)}`);
|
||||
console.log(`map1 vs map4: ${calculateVisualSimilarity(map1, map4)}`);
|
||||
console.log(`map1 vs map5: ${calculateVisualSimilarity(map1, map5)}`);
|
||||
console.log(`map2 vs map3: ${calculateVisualSimilarity(map2, map3)}`);
|
||||
console.log(`map2 vs map4: ${calculateVisualSimilarity(map2, map4)}`);
|
||||
console.log(`map2 vs map5: ${calculateVisualSimilarity(map2, map5)}`);
|
||||
console.log(`map3 vs map4: ${calculateVisualSimilarity(map3, map4)}`);
|
||||
console.log(`map3 vs map5: ${calculateVisualSimilarity(map3, map5)}`);
|
||||
console.log(`map4 vs map5: ${calculateVisualSimilarity(map4, map5)}`);
|
||||
// 测试交换性
|
||||
console.log(`map2 vs map1: ${calculateVisualSimilarity(map2, map1)}`);
|
||||
console.log(`map4 vs map2: ${calculateVisualSimilarity(map4, map2)}`);
|
||||
})();
|
||||
19
data/tsconfig.json
Normal file
19
data/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
8
data/vitest.config.ts
Normal file
8
data/vitest.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node'
|
||||
}
|
||||
});
|
||||
175
dataset.json
Normal file
175
dataset.json
Normal file
@ -0,0 +1,175 @@
|
||||
{
|
||||
"datasetId": 468713377296,
|
||||
"data": {
|
||||
"93529307366/MT1": {
|
||||
"text": [
|
||||
"左右对称结构,右侧包含一个红宝石,左侧包含一个蓝宝石,怪物全部是中等强度",
|
||||
"画一个左右对称,怪物强度中等,需要开门才能上楼的地图",
|
||||
"生成一个有两把黄钥匙,三个黄门,四个血瓶,四个怪物的左右对称地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 1, 10, 1, 5, 1],
|
||||
[1, 8, 0, 2, 0, 8, 1],
|
||||
[1, 4, 1, 6, 1, 3, 1],
|
||||
[1, 8, 6, 2, 6, 8, 1],
|
||||
[1, 5, 1, 10, 1, 5, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT2": {
|
||||
"text": [
|
||||
"画一个主干道上只有弱怪,没有宝石,包含咸鱼门的地图",
|
||||
"宝石被中等怪物守护,也可以通过开咸鱼门获得,以弱怪为主的地图",
|
||||
"这是一个主干道在地图边缘的地图,可以考虑开一个黄门提前获得宝石"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 7, 10, 0, 7, 1],
|
||||
[1, 1, 5, 1, 1, 2, 1],
|
||||
[1, 0, 7, 0, 1, 5, 1],
|
||||
[1, 6, 1, 6, 1, 7, 1],
|
||||
[1, 5, 1, 4, 8, 10, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT3": {
|
||||
"text": [
|
||||
"一个红宝石在黄门里面,门前有一个强怪守护,另一个红宝石可以打败一个中怪和两个弱怪后获得",
|
||||
"画一个主干道在中间,左侧有一个强怪守护的红宝石,右侧有一个红宝石的地图",
|
||||
"生成一个左侧怪物较强,右侧怪物较弱的地图,右侧有一个黄门捷径"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 3, 1, 10, 7, 3, 1],
|
||||
[1, 6, 1, 5, 1, 7, 1],
|
||||
[1, 9, 1, 8, 1, 6, 1],
|
||||
[1, 5, 8, 0, 1, 7, 1],
|
||||
[1, 2, 1, 5, 7, 10, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT4": {
|
||||
"text": [
|
||||
"生成一个左右异形对称,包含各种强度的怪物,有中怪守护红宝石,强怪守护蓝宝石的地图",
|
||||
"防御宝石由强怪守护,攻击宝石由中怪守护,左右不完全对称的地图",
|
||||
"画一个可以通过开门绕过一个中等强度怪物和两个弱怪,包含三个血瓶,攻防宝石各一个的地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 7, 10, 7, 0, 1],
|
||||
[1, 8, 1, 6, 1, 8, 1],
|
||||
[1, 3, 1, 5, 7, 5, 1],
|
||||
[1, 1, 1, 6, 1, 1, 1],
|
||||
[1, 10, 8, 0, 9, 4, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT5": {
|
||||
"text": [
|
||||
"画一个左上右下对称,中间全是门和宝石,周围全是怪物和血瓶的地图",
|
||||
"中间的门构成一个叉号状,空余位置填充宝石,外围包含怪物和血瓶,几乎没有空地的地图",
|
||||
"生成一个偏资源向的楼层,有很多资源集中在中间,但是需要开门才能获得,地图外围有怪物挡路"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 8, 5, 7, 0, 10, 1],
|
||||
[1, 5, 6, 4, 6, 0, 1],
|
||||
[1, 7, 3, 6, 3, 7, 1],
|
||||
[1, 2, 6, 4, 6, 5, 1],
|
||||
[1, 10, 2, 7, 5, 8, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT6": {
|
||||
"text": [
|
||||
"上楼梯在蓝门里面,需要开门才能上楼,怪物强度比较高,没有弱怪,宝石全由强怪守护,但是其中一个也可以咸门获得",
|
||||
"画一个接近左右对称的地图,最好能好看点",
|
||||
"生成一个有一把黄钥匙,怪物强度高的地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 6, 4, 1, 10, 1],
|
||||
[1, 6, 1, 9, 1, 5, 1],
|
||||
[1, 8, 0, 6, 0, 8, 1],
|
||||
[1, 5, 1, 10, 1, 2, 1],
|
||||
[1, 9, 3, 1, 4, 9, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT7": {
|
||||
"text": [
|
||||
"画一个几乎全由怪物组成,没有宝石,只有血瓶钥匙和门的地图",
|
||||
"怪物强度高,但是血瓶数量多",
|
||||
"可以直接开门上楼,没有宝石的地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 9, 2, 9, 5, 1],
|
||||
[1, 8, 0, 10, 0, 8, 1],
|
||||
[1, 5, 9, 6, 9, 5, 1],
|
||||
[1, 0, 8, 10, 8, 0, 1],
|
||||
[1, 6, 0, 5, 0, 6, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT8": {
|
||||
"text": [
|
||||
"以走廊为主,没有强怪,只有弱怪和中怪,有一定数量的宝石的地图",
|
||||
"画一个蓝海风,以走廊为主的地图",
|
||||
"生成一个资源比较丰富,有宝石、血瓶,需要开门才能上楼,有一些咸鱼门的地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 8, 10, 7, 2, 1],
|
||||
[1, 2, 1, 5, 1, 7, 1],
|
||||
[1, 3, 1, 3, 6, 4, 1],
|
||||
[1, 6, 1, 6, 1, 8, 1],
|
||||
[1, 10, 7, 5, 1, 5, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT9": {
|
||||
"text": [
|
||||
"左右基本对称,但是入口不对称,宝石种类不对称的地图",
|
||||
"画一个左右基本对称,没有强怪的地图",
|
||||
"生成一个有两个红宝石,一个蓝宝石,很多血瓶,没有强怪的地图"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 4, 1, 10, 1, 3, 1],
|
||||
[1, 5, 7, 0, 7, 5, 1],
|
||||
[1, 8, 6, 5, 6, 8, 1],
|
||||
[1, 5, 8, 6, 8, 5, 1],
|
||||
[1, 10, 5, 1, 5, 3, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
},
|
||||
"93529307366/MT10": {
|
||||
"text": [
|
||||
"左右对称的 Boss 层",
|
||||
"画一个 Boss 层,入口在上方,Boss 在下方,中间有几个弱怪挡路",
|
||||
"生成一个 Boss 层,可以咸门提前拿资源"
|
||||
],
|
||||
"map": [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 5, 1, 10, 1, 5, 1],
|
||||
[1, 6, 7, 7, 7, 6, 1],
|
||||
[1, 1, 6, 5, 6, 1, 1],
|
||||
[1, 4, 5, 9, 5, 4, 1],
|
||||
[1, 3, 1, 1, 1, 3, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1]
|
||||
],
|
||||
"size": [7, 7]
|
||||
}
|
||||
}
|
||||
}
|
||||
0
ginka/__init__.py
Normal file
0
ginka/__init__.py
Normal file
54
ginka/dataset.py
Normal file
54
ginka/dataset.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
import random
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from transformers import BertTokenizer
|
||||
|
||||
def load_data(path: str):
|
||||
with open(path, 'r', encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
data_list = []
|
||||
for value in data["data"].values():
|
||||
data_list.append(value)
|
||||
|
||||
return data_list
|
||||
|
||||
class GinkaDataset(Dataset):
|
||||
def __init__(self, data_path: str, tokenizer: BertTokenizer, max_len=128):
|
||||
self.data = load_data(data_path) # 自定义数据加载函数
|
||||
self.tokenizer = tokenizer
|
||||
self.max_len = max_len
|
||||
self.max_size = 32
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.data[idx]
|
||||
|
||||
# 文本处理
|
||||
text = random.choice(item["text"])
|
||||
encoding = self.tokenizer(
|
||||
text,
|
||||
max_length=self.max_len,
|
||||
padding="max_length",
|
||||
truncation=True,
|
||||
return_tensors="pt"
|
||||
)
|
||||
|
||||
# 噪声生成
|
||||
w, h = item["size"]
|
||||
noise = torch.randn(h, w, 1)
|
||||
|
||||
# 目标矩阵填充
|
||||
target = torch.full((self.max_size, self.max_size), -100) # 使用-100忽略填充区域
|
||||
target[:h, :w] = torch.tensor(item["map"])
|
||||
|
||||
return {
|
||||
"noise": noise,
|
||||
"input_ids": encoding["input_ids"].squeeze(),
|
||||
"attention_mask": encoding["attention_mask"].squeeze(),
|
||||
"map_size": torch.tensor([h, w]),
|
||||
"target": target
|
||||
}
|
||||
338
ginka/model/loss.py
Normal file
338
ginka/model/loss.py
Normal file
@ -0,0 +1,338 @@
|
||||
import math
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from pytorch_toolbelt import losses as L
|
||||
|
||||
def wall_border_loss(pred: torch.Tensor, probs: torch.Tensor, allow_border=[1, 11]):
|
||||
"""地图最外层是否为墙"""
|
||||
# 计算 softmax 概率
|
||||
B, C, H, W = pred.shape
|
||||
|
||||
# 构造一个 [H, W] 的布尔 mask,选取最外圈的像素
|
||||
border_mask = torch.zeros((H, W), dtype=torch.bool, device=pred.device)
|
||||
border_mask[0, :] = True
|
||||
border_mask[-1, :] = True
|
||||
border_mask[:, 0] = True
|
||||
border_mask[:, -1] = True
|
||||
|
||||
# 对允许的类别求概率和(即该像素为允许类别的总概率)
|
||||
allowed_prob = probs[:, allow_border, :, :].sum(dim=1) # [B, H, W]
|
||||
|
||||
# 只计算边界区域的损失:对于边界上的每个像素,要求 allowed_prob 越高越好
|
||||
border_allowed_prob = allowed_prob[:, border_mask] # [B, N_border_pixels]
|
||||
|
||||
# 损失为 -log(allowed_prob)
|
||||
loss = 1 - border_allowed_prob.mean()
|
||||
|
||||
return loss
|
||||
|
||||
def internal_wall_loss(logits, probs, wall_class=1, threshold=2.5):
|
||||
"""
|
||||
针对内部区域(排除最外圈)设计的损失函数:
|
||||
当内部任意 2×2 区域的 wall 类别概率之和超过阈值时,施加惩罚。
|
||||
|
||||
参数:
|
||||
logits: 模型输出,形状 [B, C, H, W]
|
||||
wall_class: 对应墙壁的类别索引(这里假设墙壁数字为1)
|
||||
threshold: 2×2 区域概率之和的阈值,超过此值时施加惩罚。可根据实际情况调节。
|
||||
|
||||
返回:
|
||||
loss: 内部墙壁连续区域的平均惩罚损失
|
||||
"""
|
||||
# 取出对应墙壁类别的概率图 [B, H, W]
|
||||
wall_probs = probs[:, wall_class, :, :]
|
||||
|
||||
# 排除最外圈,取内部区域 (H, W 均减去2)
|
||||
interior = wall_probs[:, 1:-1, 1:-1] # [B, H-2, W-2]
|
||||
|
||||
# 构造一个 2×2 的卷积核,全为 1,用于检测局部连续墙壁的概率之和
|
||||
kernel = torch.ones((1, 1, 2, 2), device=logits.device)
|
||||
|
||||
# 对内部区域进行卷积操作,计算每个 2×2 区域内的概率和
|
||||
# 需要将 interior 扩展一个通道维度
|
||||
conv_result = F.conv2d(interior.unsqueeze(1), kernel, stride=1, padding=0)
|
||||
# conv_result 的形状为 [B, 1, H-3, W-3]
|
||||
|
||||
# 对于每个 2×2 区域,如果概率和超过 threshold,则产生惩罚
|
||||
# 这里采用 ReLU 计算超出部分,确保损失为非负
|
||||
penalty = F.relu(conv_result - threshold)
|
||||
|
||||
# 取平均作为损失值
|
||||
loss = penalty.mean()
|
||||
return loss
|
||||
|
||||
def entrance_loss(logits, probs, stairs_class=10, arrow_class=11):
|
||||
"""
|
||||
针对地图生成的额外约束损失:
|
||||
- 保证最外圈不出现楼梯类型入口(数字10)
|
||||
- 保证内部区域不出现箭头类型入口(数字11)
|
||||
|
||||
参数:
|
||||
logits: 模型输出,形状 [B, C, H, W]
|
||||
stairs_class: 楼梯入口对应的类别(数字10)
|
||||
arrow_class: 箭头入口对应的类别(数字11)
|
||||
|
||||
返回:
|
||||
loss: 针对入口出现的惩罚损失
|
||||
"""
|
||||
# 先将 logits 转为概率分布
|
||||
B, C, H, W = logits.shape
|
||||
|
||||
# 构造最外圈 mask:外圈为 True,其余为 False
|
||||
outer_mask = torch.zeros((H, W), dtype=torch.bool, device=logits.device)
|
||||
outer_mask[0, :] = True
|
||||
outer_mask[-1, :] = True
|
||||
outer_mask[:, 0] = True
|
||||
outer_mask[:, -1] = True
|
||||
|
||||
# 内部区域 mask
|
||||
interior_mask = ~outer_mask # 取反
|
||||
|
||||
# 提取对应类别的概率图
|
||||
stairs_probs = probs[:, stairs_class, :, :] # 楼梯概率 [B, H, W]
|
||||
arrow_probs = probs[:, arrow_class, :, :] # 箭头概率 [B, H, W]
|
||||
|
||||
# 从最外圈提取楼梯概率;用 mask 索引时:张量[:, mask] 会将每个样本的外圈像素展平
|
||||
outer_stairs = stairs_probs[:, outer_mask] # [B, num_outer_pixels]
|
||||
# 从内部区域提取箭头概率
|
||||
interior_arrow = arrow_probs[:, interior_mask] # [B, num_interior_pixels]
|
||||
|
||||
# 损失设计:使得这些概率尽量接近 0,直接使用均值惩罚
|
||||
outer_loss = outer_stairs.mean()
|
||||
interior_loss = interior_arrow.mean()
|
||||
|
||||
total_loss = outer_loss + interior_loss
|
||||
return total_loss
|
||||
|
||||
def entrance_distance_and_presence_loss(
|
||||
logits, probs,
|
||||
arrow_class=11, stairs_class=10,
|
||||
arrow_min_threshold=0.5, stairs_min_threshold=0.5,
|
||||
lambda_arrow_presence=1.0, lambda_stairs_presence=1.0
|
||||
):
|
||||
"""
|
||||
入口损失同时考虑:
|
||||
1. 局部距离约束:防止同一类型入口过于靠近
|
||||
2. 存在性约束:鼓励至少放置一个入口
|
||||
|
||||
箭头入口要求局部 (9x9) 内最多只有一个入口;
|
||||
楼梯入口要求在一个窗口(地图尺寸一半)内只出现一个楼梯入口。
|
||||
|
||||
参数:
|
||||
logits: 模型输出, shape [B, C, H, W]
|
||||
arrow_class: 箭头入口类别(默认 11)
|
||||
stairs_class: 楼梯入口类别(默认 10)
|
||||
arrow_min_threshold: 箭头入口全局最小平均概率要求(可根据任务调节)
|
||||
stairs_min_threshold: 楼梯入口全局最小平均概率要求
|
||||
lambda_arrow_presence: 箭头入口存在性损失权重
|
||||
lambda_stairs_presence: 楼梯入口存在性损失权重
|
||||
返回:
|
||||
total_loss: 综合入口距离与存在性损失
|
||||
"""
|
||||
# 将 logits 转换为概率分布
|
||||
probs = F.softmax(logits, dim=1) # [B, C, H, W]
|
||||
B, C, H, W = logits.shape
|
||||
|
||||
# 提取箭头和楼梯的概率图
|
||||
arrow_probs = probs[:, arrow_class, :, :] # [B, H, W]
|
||||
stairs_probs = probs[:, stairs_class, :, :] # [B, H, W]
|
||||
|
||||
#### 局部距离约束 ####
|
||||
# 箭头:构造 9x9 卷积核,半径 4
|
||||
kernel_arrow = torch.ones((1, 1, 9, 9), device=logits.device)
|
||||
local_arrow_sum = F.conv2d(arrow_probs.unsqueeze(1), kernel_arrow, padding=4)
|
||||
# 减去自身概率,计算多余的局部累积
|
||||
arrow_excess = local_arrow_sum - arrow_probs.unsqueeze(1)
|
||||
arrow_distance_loss = F.relu(arrow_excess).mean()
|
||||
|
||||
# 楼梯:使用窗口大小为 (W//2, H//2)
|
||||
kernel_size_stairs = (max(1, W // 2), max(1, H // 2))
|
||||
kernel_stairs = torch.ones((1, 1, kernel_size_stairs[0], kernel_size_stairs[1]), device=logits.device)
|
||||
pad_stairs = (kernel_size_stairs[0] // 2, kernel_size_stairs[1] // 2)
|
||||
local_stairs_sum = F.conv2d(stairs_probs.unsqueeze(1), kernel_stairs, padding=pad_stairs)
|
||||
stairs_excess = local_stairs_sum - stairs_probs.unsqueeze(1)
|
||||
stairs_distance_loss = F.relu(stairs_excess).mean()
|
||||
|
||||
#### 存在性约束 ####
|
||||
# 计算每个样本中箭头的最大概率
|
||||
global_arrow_max = arrow_probs.view(B, -1).max(dim=1)[0] # [B]
|
||||
global_stairs_max = stairs_probs.view(B, -1).max(dim=1)[0] # [B]
|
||||
|
||||
# 取 batch 平均(或者你可以对每个样本分别计算损失再求平均)
|
||||
global_arrow_max = global_arrow_max.mean()
|
||||
global_stairs_max = global_stairs_max.mean()
|
||||
|
||||
# 如果全局均值低于预期阈值,则施加额外惩罚
|
||||
arrow_presence_loss = F.relu(arrow_min_threshold - global_arrow_max)
|
||||
stairs_presence_loss = F.relu(stairs_min_threshold - global_stairs_max)
|
||||
|
||||
ap_weighted = lambda_arrow_presence * arrow_presence_loss
|
||||
sp_weighted = lambda_stairs_presence * stairs_presence_loss
|
||||
|
||||
# 总入口损失:局部距离约束 + 存在性约束(加权)
|
||||
total_loss = arrow_distance_loss + stairs_distance_loss \
|
||||
+ min(ap_weighted, sp_weighted)
|
||||
return total_loss
|
||||
|
||||
def monster_consecutive_loss(logits, probs, monster_classes=[7,8,9], threshold=2.9):
|
||||
"""
|
||||
检查横向和纵向是否存在连续超过三个的怪物(类别 7,8,9)。
|
||||
|
||||
参数:
|
||||
logits: 模型输出,形状 [B, C, H, W]
|
||||
monster_classes: 待检测的怪物类别列表
|
||||
threshold: 滑动窗口内概率和的阈值,若超过则施加惩罚
|
||||
(对于连续三个像素,如果每个像素概率接近 1,则窗口和接近 3)
|
||||
|
||||
返回:
|
||||
loss: 惩罚损失(数值越高表示连续怪物区域越严重)
|
||||
"""
|
||||
# 将 logits 转换为概率分布
|
||||
B, C, H, W = logits.shape
|
||||
|
||||
# 得到怪物整体概率图:将类别 7,8,9 的概率相加
|
||||
monster_probs = probs[:, monster_classes, :].sum(dim=1) # [B, H, W]
|
||||
|
||||
# 注意:monster_probs 越高说明该像素更有可能是怪物
|
||||
|
||||
# --- 横向检测 ---
|
||||
# 构造一个 (1,3) 的卷积核,全 1
|
||||
kernel_horiz = torch.ones((1, 1, 1, 3), device=logits.device)
|
||||
# 对 monster_probs 加一个 channel 维度,使形状为 [B, 1, H, W]
|
||||
conv_horiz = F.conv2d(monster_probs.unsqueeze(1), kernel_horiz, padding=(0,1))
|
||||
# conv_horiz 的每个值表示相邻三个像素的怪物概率和
|
||||
|
||||
# --- 纵向检测 ---
|
||||
# 构造一个 (3,1) 的卷积核,全 1
|
||||
kernel_vert = torch.ones((1, 1, 3, 1), device=logits.device)
|
||||
conv_vert = F.conv2d(monster_probs.unsqueeze(1), kernel_vert, padding=(1,0))
|
||||
# conv_vert 的每个值表示垂直连续三个像素的怪物概率和
|
||||
|
||||
# 对两个方向的窗口,如果概率和超过阈值,则计算超出部分的惩罚
|
||||
penalty_horiz = F.relu(conv_horiz - threshold)
|
||||
penalty_vert = F.relu(conv_vert - threshold)
|
||||
|
||||
# 将两个方向的惩罚损失取平均(或者直接相加)
|
||||
loss = penalty_horiz.mean() + penalty_vert.mean()
|
||||
return loss
|
||||
|
||||
def illegal_block_loss(logits ,probs, used_classes=12, mode='mean'):
|
||||
"""
|
||||
对未使用类别(例如 12 ~ 31)的预测概率施加惩罚,
|
||||
鼓励模型输出仅集中在 0 ~ 11 上。
|
||||
|
||||
参数:
|
||||
logits: 模型输出,形状 [B, num_classes, H, W]
|
||||
used_classes: 已经使用的类别数(例如 12 表示只使用 0-11)
|
||||
mode: 'mean' 使用平均概率,或 'mse' 使用均方误差
|
||||
|
||||
返回:
|
||||
penalty: 标量惩罚损失
|
||||
"""
|
||||
# 选取非法类别的概率(注意:这一步会得到非法图块在每个像素上的概率)
|
||||
illegal_probs = probs[:, range(used_classes, 32), :, :] # [B, len(illegal_classes), H, W]
|
||||
|
||||
# 我们可以将非法图块的概率在类别维度上求和,得到每个像素的非法激活值
|
||||
illegal_activation = illegal_probs.sum(dim=1) # [B, H, W]
|
||||
|
||||
# 接下来我们计算整个图上非法激活的“数量”
|
||||
# 例如,可以直接对整个 batch 内非法激活求和
|
||||
total_illegal = illegal_activation.sum() # 标量
|
||||
|
||||
# 计算损失值:使用负指数函数。注意如果非法激活很小,总损失接近 exp(0)=1
|
||||
loss = torch.sqrt(total_illegal)
|
||||
return loss
|
||||
|
||||
def integrated_count_loss(probs, target, class_list=[0,1,2,3,4,5,6,7,8,9], tolerance=0.5):
|
||||
"""
|
||||
对每个类别分别计算数量匹配损失,再取平均。
|
||||
|
||||
参数:
|
||||
probs: 模型输出的概率,形状 [B, num_classes, H, W]
|
||||
target: 真实标签,形状 [B, H, W],类别取值在 0 ~ 使用范围-1 内
|
||||
class_list: 需要计算的类别列表
|
||||
tolerance: 每个类别允许的相对误差(例如 0.15 表示 15%)
|
||||
|
||||
返回:
|
||||
loss: 对每个类别数量匹配损失取平均后的标量
|
||||
"""
|
||||
total_loss = 0.0
|
||||
count = 0
|
||||
B, C, H, W = probs.shape
|
||||
|
||||
for cls in class_list:
|
||||
# 预测数量:对于当前类别,所有像素的预测概率和
|
||||
pred_count = probs[:, cls, :, :].sum()
|
||||
# 真实数量:统计 target 中属于当前类别的像素数量
|
||||
true_count = (target == cls).float().sum()
|
||||
|
||||
if true_count == 0:
|
||||
# 参考地图中不包含该类别,允许最多出现 (sqrt(地图尺寸) / 2) 个单位的概率输出
|
||||
cls_loss = F.relu(pred_count - math.sqrt(H * W) / 2)
|
||||
else:
|
||||
# 计算相对误差
|
||||
rel_error = torch.abs(pred_count - true_count) / (true_count)
|
||||
cls_loss = F.relu(rel_error - tolerance)
|
||||
|
||||
total_loss += cls_loss
|
||||
count += 1
|
||||
|
||||
# 求平均每个类别的损失
|
||||
avg_loss = total_loss / count
|
||||
return avg_loss
|
||||
|
||||
class GinkaLoss(nn.Module):
|
||||
def __init__(self, weight=[0.35, 0.1, 0.1, 0.1, 0.1, 0.05, 0.1, 0.1]):
|
||||
"""Ginka Model 损失函数部分
|
||||
|
||||
Args:
|
||||
weight (list, optional): 每一个损失函数的权重,从第 0 项开始,依次是:
|
||||
1. 拓扑图损失
|
||||
2. 外圈墙壁损失
|
||||
3. 内层 2*2 墙壁损失
|
||||
4. 要求外层只能有箭头,内层只能有楼梯的损失
|
||||
5. 入口间距及存在性损失
|
||||
6. 连续怪物损失
|
||||
7. 非法图块损失
|
||||
8. 怪物、道具、门数量损失
|
||||
"""
|
||||
super().__init__()
|
||||
self.weight = weight
|
||||
self.dice = L.DiceLoss(mode='multiclass')
|
||||
self.ce = nn.CrossEntropyLoss()
|
||||
|
||||
def forward(self, pred, target):
|
||||
probs = F.softmax(pred, dim=1)
|
||||
# 拓扑结构损失
|
||||
# structure_loss = topology_loss(pred, target)
|
||||
# 地图结构损失
|
||||
border_loss = wall_border_loss(pred, probs)
|
||||
wall_loss = internal_wall_loss(pred, probs)
|
||||
entry_loss = entrance_loss(pred, probs)
|
||||
entry_dis_loss = entrance_distance_and_presence_loss(pred, probs)
|
||||
enemy_loss = monster_consecutive_loss(pred, probs)
|
||||
valid_block_loss = illegal_block_loss(pred, probs, used_classes=12, mode="mean")
|
||||
count_loss = integrated_count_loss(probs, target)
|
||||
|
||||
print(
|
||||
# structure_loss.item(),
|
||||
border_loss.item(),
|
||||
wall_loss.item(),
|
||||
entry_loss.item(),
|
||||
entry_dis_loss.item(),
|
||||
enemy_loss.item(),
|
||||
valid_block_loss.item(),
|
||||
count_loss.item()
|
||||
)
|
||||
|
||||
return (
|
||||
# structure_loss * self.weight[0] +
|
||||
border_loss * self.weight[1] +
|
||||
wall_loss * self.weight[2] +
|
||||
entry_loss * self.weight[3] +
|
||||
entry_dis_loss * self.weight[4] +
|
||||
enemy_loss * self.weight[5] +
|
||||
valid_block_loss * self.weight[6] +
|
||||
count_loss * self.weight[7]
|
||||
)
|
||||
180
ginka/model/model.py
Normal file
180
ginka/model/model.py
Normal file
@ -0,0 +1,180 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from transformers import BertModel
|
||||
from ...shared.attention import CBAM, SpatialAttention
|
||||
from .sample import HybridUpsample, FinalUpsample, GumbelSampler
|
||||
|
||||
class ResidualBlock(nn.Module):
|
||||
"""残差块"""
|
||||
def __init__(self, channels):
|
||||
super().__init__()
|
||||
self.conv = nn.Sequential(
|
||||
nn.Conv2d(channels, channels, 3, padding=1),
|
||||
nn.GroupNorm(8, channels),
|
||||
nn.GELU(),
|
||||
nn.Conv2d(channels, channels, 3, padding=1),
|
||||
nn.GroupNorm(8, channels)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return x + self.conv(x)
|
||||
|
||||
class DynamicPadConv(nn.Module):
|
||||
"""支持动态处理奇数尺寸的智能卷积"""
|
||||
def __init__(self, in_ch, out_ch, kernel=3, stride=1):
|
||||
super().__init__()
|
||||
self.conv = nn.Conv2d(
|
||||
in_ch, out_ch, kernel,
|
||||
stride=stride,
|
||||
padding=kernel//2
|
||||
)
|
||||
self.requires_pad = (stride > 1) # 仅在下采样时需要填充
|
||||
|
||||
def forward(self, x):
|
||||
if self.requires_pad:
|
||||
# 动态计算各维度需要填充的量
|
||||
pad_h = x.size(-2) % 2
|
||||
pad_w = x.size(-1) % 2
|
||||
if pad_h or pad_w:
|
||||
x = F.pad(x, (0, pad_w, 0, pad_h)) # 右下填充
|
||||
return self.conv(x)
|
||||
|
||||
class ConditionInjector(nn.Module):
|
||||
"""基于注意力机制的条件注入"""
|
||||
def __init__(self, cond_dim=128, feat_dim=256):
|
||||
super().__init__()
|
||||
self.cond_proj = nn.Sequential(
|
||||
nn.Linear(cond_dim, feat_dim * 2),
|
||||
nn.GELU(),
|
||||
nn.LayerNorm(feat_dim * 2)
|
||||
)
|
||||
self.channel_att = nn.Sequential(
|
||||
nn.Conv2d(feat_dim, feat_dim//8, 1),
|
||||
nn.GELU(),
|
||||
nn.Conv2d(feat_dim//8, feat_dim, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
def forward(self, x, cond):
|
||||
# 投影条件向量
|
||||
gamma, beta = self.cond_proj(cond).chunk(2, dim=1) # [B, D]
|
||||
|
||||
# 通道注意力调制
|
||||
att = self.channel_att(x) # [B, C, H, W]
|
||||
modulated = x * att
|
||||
|
||||
# 添加条件偏置
|
||||
return modulated + beta.view(-1, gamma.size(1), 1, 1)
|
||||
|
||||
class GinkaEncoder(nn.Module):
|
||||
"""编码器(下采样)部分"""
|
||||
def __init__(self, in_ch, out_ch):
|
||||
super().__init__()
|
||||
self.encoder = nn.Sequential(
|
||||
DynamicPadConv(in_ch, out_ch, stride=1),
|
||||
ResidualBlock(out_ch),
|
||||
CBAM(out_ch),
|
||||
nn.GroupNorm(8, out_ch),
|
||||
nn.GELU()
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.encoder(x)
|
||||
|
||||
class GinkaModel(nn.Module):
|
||||
def __init__(self, in_ch=1, base_ch=64, num_classes=32):
|
||||
"""Ginka Model 模型定义部分
|
||||
|
||||
Args:
|
||||
in_ch (int, optional): 输入通道数,默认是 1
|
||||
base_ch (int, optional): UNet 上下采样卷积基础通道数,默认 64
|
||||
num_classes (int, optional): 图块种类数量,默认 32 预留出一部分以供后续拓展功能
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# 轻量级文本编码器(使用BERT前4层)
|
||||
self.bert = BertModel.from_pretrained('google-bert/bert-base-chinese', output_hidden_states=True)
|
||||
self.text_proj = nn.Linear(768, 128)
|
||||
|
||||
# 动态尺寸处理系统
|
||||
self.size_embed = nn.Embedding(32, 16) # 处理最大32的尺寸
|
||||
|
||||
# 编码器
|
||||
self.enc1 = GinkaEncoder(in_ch, base_ch)
|
||||
self.enc2 = GinkaEncoder(base_ch, base_ch * 2)
|
||||
# self.enc3 = GinkaEncoder(base_ch * 2, base_ch * 4)
|
||||
|
||||
# 中间层
|
||||
self.mid = nn.Sequential(
|
||||
DynamicPadConv(base_ch * 2, base_ch * 4),
|
||||
ConditionInjector(160, base_ch * 4)
|
||||
)
|
||||
|
||||
# 解码器,解码器仅使用空间注意力
|
||||
self.dec1 = HybridUpsample(base_ch * 4, base_ch * 2)
|
||||
self.dec1_att = SpatialAttention()
|
||||
|
||||
self.dec2 = HybridUpsample(base_ch * 2, base_ch)
|
||||
self.dec2_att = SpatialAttention()
|
||||
|
||||
# self.dec3 = HybridUpsample(base_ch * 2, base_ch)
|
||||
# self.dec3_att = SpatialAttention()
|
||||
|
||||
# 输出层
|
||||
self.out = FinalUpsample(base_ch, num_classes)
|
||||
|
||||
def forward(self, noise, input_ids, attention_mask, map_size):
|
||||
"""
|
||||
Args:
|
||||
noise: 噪声输入 [BS, H, W, 1]
|
||||
input_ids: 文本token id [BS, seq_len]
|
||||
attention_mask: 文本attention mask [BS, seq_len]
|
||||
map_size: 地图尺寸 [BS, 2] (height, width)
|
||||
Returns:
|
||||
logits: 输出logits [BS, num_classes, H, W]
|
||||
"""
|
||||
# 文本特征提取
|
||||
with torch.no_grad(): # 冻结BERT参数
|
||||
bert_outputs = self.bert(
|
||||
input_ids=input_ids,
|
||||
attention_mask=attention_mask,
|
||||
output_hidden_states=True
|
||||
)
|
||||
# 取前4层隐藏状态的平均
|
||||
hidden_states = torch.stack(bert_outputs.hidden_states[1:5]) # [4, BS, seq_len, 768]
|
||||
text_features = torch.mean(hidden_states, dim=0)[:, 0, :] # [BS, 768]
|
||||
text_features = self.text_proj(text_features) # [BS, 128]
|
||||
|
||||
# 尺寸特征处理
|
||||
h_emb = self.size_embed(map_size[:, 0]) # [BS, 16]
|
||||
w_emb = self.size_embed(map_size[:, 1]) # [BS, 16]
|
||||
size_features = torch.cat([h_emb, w_emb], dim=1) # [BS, 32]
|
||||
|
||||
# 特征融合
|
||||
conditional = torch.cat([text_features, size_features], dim=1) # [BS, 160]
|
||||
|
||||
# 调整噪声输入维度
|
||||
x = noise.permute(0, 3, 1, 2) # [BS, 1, H, W]
|
||||
|
||||
# 编码器路径
|
||||
x1 = self.enc1(x) # [BS, 64, H / 2, W / 2]
|
||||
x2 = self.enc2(x1) # [BS, 128, H / 4, W / 4]
|
||||
|
||||
# 中间层(注入条件)
|
||||
x_mid = self.mid[0](x2) # [BS, 256, H / 4, W / 4]
|
||||
x_mid = self.mid[1](x_mid, conditional)
|
||||
|
||||
# 解码器路径
|
||||
d1 = self.dec1(x_mid, x2) # [BS, 128, H / 2, W / 2]
|
||||
d1 = self.dec1_att(d1)
|
||||
d2 = self.dec2(d1, x1) # [BS, 64, H, W]
|
||||
d2 = self.dec2_att(d2)
|
||||
# d3 = self.dec3(d2, x1)
|
||||
# d3 = self.dec3_att(d3)
|
||||
|
||||
# 最终自适应上采样
|
||||
h, w = noise.shape[1:3] # 获取原始输入尺寸
|
||||
return self.out(d2, (h, w))
|
||||
|
||||
|
||||
79
ginka/model/sample.py
Normal file
79
ginka/model/sample.py
Normal file
@ -0,0 +1,79 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision.ops as ops
|
||||
|
||||
class HybridUpsample(nn.Module):
|
||||
"""自适应尺寸的混合上采样"""
|
||||
def __init__(self, in_ch, out_ch, skip_ch=None):
|
||||
super().__init__()
|
||||
# 子像素卷积上采样
|
||||
self.subpixel = nn.Sequential(
|
||||
nn.Conv2d(in_ch, out_ch * 4, 3, padding=1),
|
||||
nn.PixelShuffle(2) # 2倍上采样
|
||||
)
|
||||
|
||||
# 跳跃连接处理
|
||||
self.skip_conv = nn.Conv2d(skip_ch, out_ch, 1) if skip_ch else None
|
||||
self.adaptive_pool = nn.AdaptiveAvgPool2d(None)
|
||||
|
||||
def forward(self, x, skip=None):
|
||||
x = self.subpixel(x) # [B, out_ch, 2H, 2W]
|
||||
|
||||
if skip is not None and self.skip_conv:
|
||||
# 自动对齐尺寸
|
||||
if x.shape[-2:] != skip.shape[-2:]:
|
||||
skip = F.interpolate(skip, size=x.shape[-2:], mode='nearest')
|
||||
|
||||
# 融合特征
|
||||
x = x + self.skip_conv(skip)
|
||||
|
||||
return x
|
||||
|
||||
class DiscreteAwareUpsample(nn.Module):
|
||||
"""离散感知的智能上采样模块"""
|
||||
def __init__(self, in_ch, out_ch, base_size=16):
|
||||
super().__init__()
|
||||
self.base_size = base_size
|
||||
self.scale_factors = [2, 4, 8] # 支持放大倍数
|
||||
|
||||
# 可变形卷积增强几何感知
|
||||
self.deform_conv = ops.DeformConv2d(in_ch, in_ch, kernel_size=3, padding=1)
|
||||
|
||||
# 多尺度特征融合
|
||||
self.multi_scale = nn.ModuleList([
|
||||
nn.Sequential(
|
||||
nn.Conv2d(in_ch, in_ch//4, 1),
|
||||
nn.Upsample(scale_factor=s, mode='nearest')
|
||||
) for s in self.scale_factors
|
||||
])
|
||||
|
||||
# 门控上采样机制
|
||||
self.gate_conv = nn.Conv2d(in_ch*2, len(self.scale_factors)+1, 3, padding=1)
|
||||
|
||||
# 离散化输出层
|
||||
self.final_conv = nn.Sequential(
|
||||
nn.Conv2d(in_ch, out_ch*4, 3, padding=1),
|
||||
nn.PixelShuffle(2), # 亚像素卷积
|
||||
nn.Conv2d(out_ch, out_ch, 3, padding=1)
|
||||
)
|
||||
|
||||
def forward(self, x, target_size):
|
||||
# 几何特征提取
|
||||
deform_feat = self.deform_conv(x)
|
||||
|
||||
# 生成多尺度特征
|
||||
scale_features = [f(deform_feat) for f in self.multi_scale]
|
||||
|
||||
# 动态门控选择
|
||||
gate_map = F.softmax(self.gate_conv(torch.cat([x, deform_feat], dim=1)), dim=1)
|
||||
|
||||
# 加权融合多尺度特征
|
||||
combined = sum(g * F.interpolate(f, size=target_size, mode='nearest')
|
||||
for g, f in zip(gate_map.unbind(1), scale_features+[x]))
|
||||
|
||||
# 离散化上采样
|
||||
out = self.final_conv(combined)
|
||||
|
||||
# 结构化约束(保持通道独立性)
|
||||
return out.argmax(dim=1).unsqueeze(1).float() # 伪梯度保留
|
||||
0
ginka/test/__init__.py
Normal file
0
ginka/test/__init__.py
Normal file
46
ginka/test/model.py
Normal file
46
ginka/test/model.py
Normal file
@ -0,0 +1,46 @@
|
||||
import torch
|
||||
from ..model.model import DynamicPadConv, ConditionInjector, HybridUpsample
|
||||
|
||||
def test_dynamic_conv():
|
||||
conv = DynamicPadConv(3, 64, stride=2)
|
||||
|
||||
# 测试奇数尺寸
|
||||
x = torch.randn(1, 3, 15, 17)
|
||||
out = conv(x)
|
||||
assert out.shape == (1, 64, 8, 9), f"Got {out.shape}"
|
||||
|
||||
# 测试偶数尺寸
|
||||
x = torch.randn(1, 3, 16, 16)
|
||||
out = conv(x)
|
||||
assert out.shape == (1, 64, 8, 8)
|
||||
|
||||
def test_condition_injector():
|
||||
injector = ConditionInjector(128, 256)
|
||||
x = torch.randn(2, 256, 16, 16)
|
||||
cond = torch.randn(2, 128)
|
||||
|
||||
out = injector(x, cond)
|
||||
assert out.shape == x.shape
|
||||
assert not torch.allclose(out, x) # 确保条件起作用
|
||||
|
||||
def test_hybrid_upsample():
|
||||
# 带跳跃连接的情况
|
||||
upsample = HybridUpsample(256, 128, skip_ch=64)
|
||||
x = torch.randn(2, 256, 8, 8)
|
||||
skip = torch.randn(2, 64, 16, 16)
|
||||
out = upsample(x, skip)
|
||||
assert out.shape == (2, 128, 16, 16)
|
||||
|
||||
# 无跳跃连接的情况
|
||||
upsample = HybridUpsample(256, 128)
|
||||
out = upsample(x)
|
||||
assert out.shape == (2, 128, 16, 16)
|
||||
|
||||
def test_all():
|
||||
test_dynamic_conv()
|
||||
print("✅ 动态卷积测试完毕")
|
||||
test_condition_injector()
|
||||
print("✅ 条件注入测试完毕")
|
||||
test_hybrid_upsample()
|
||||
print("✅ 混合上采样测试完毕")
|
||||
|
||||
4
ginka/test_model.py
Normal file
4
ginka/test_model.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .test.model import test_all
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_all()
|
||||
113
ginka/train.py
Normal file
113
ginka/train.py
Normal file
@ -0,0 +1,113 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
import torch
|
||||
import torch.optim as optim
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.data import DataLoader
|
||||
from transformers import BertTokenizer
|
||||
from tqdm import tqdm
|
||||
from .model.model import GinkaModel
|
||||
from .model.loss import GinkaLoss
|
||||
from .dataset import GinkaDataset
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
os.makedirs("result", exist_ok=True)
|
||||
|
||||
epochs = 100
|
||||
|
||||
def collate_fn(batch):
|
||||
# 动态填充噪声到最大尺寸
|
||||
max_h = max([b["noise"].shape[0] for b in batch])
|
||||
max_w = max([b["noise"].shape[1] for b in batch])
|
||||
|
||||
padded_batch = {}
|
||||
for key in ["noise", "target"]:
|
||||
padded = []
|
||||
for b in batch:
|
||||
tensor = b[key]
|
||||
pad_h = max_h - tensor.shape[0]
|
||||
pad_w = max_w - tensor.shape[1]
|
||||
padded.append(F.pad(tensor, (0, pad_w, 0, pad_h), value=-100 if key=="target" else 0))
|
||||
padded_batch[key] = torch.stack(padded)
|
||||
|
||||
# 其他字段直接堆叠
|
||||
for key in ["input_ids", "attention_mask", "map_size"]:
|
||||
padded_batch[key] = torch.stack([b[key] for b in batch])
|
||||
|
||||
return padded_batch
|
||||
|
||||
def train():
|
||||
print(f"Using {"cuda" if torch.cuda.is_available() else "cpu"} to train model.")
|
||||
model = GinkaModel()
|
||||
model.to(device)
|
||||
|
||||
# 准备数据集
|
||||
tokenizer = BertTokenizer.from_pretrained('google-bert/bert-base-chinese')
|
||||
dataset = GinkaDataset("F:/github-ai/ginka-generator/dataset.json", tokenizer)
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=4,
|
||||
shuffle=True,
|
||||
collate_fn=collate_fn,
|
||||
num_workers=0
|
||||
)
|
||||
|
||||
# 设定优化器与调度器
|
||||
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
||||
criterion = GinkaLoss()
|
||||
|
||||
# 开始训练
|
||||
for epoch in tqdm(range(epochs)):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
|
||||
# 温度退火
|
||||
model.gumbel.tau = max(0.1, 1.0 - 0.9 * epoch / epochs)
|
||||
|
||||
for batch in dataloader:
|
||||
# 数据迁移到设备
|
||||
noise = batch["noise"].to(device)
|
||||
input_ids = batch["input_ids"].to(device)
|
||||
attention_mask = batch["attention_mask"].to(device)
|
||||
map_size = batch["map_size"].to(device)
|
||||
target = batch["target"].to(device)
|
||||
|
||||
# 前向传播
|
||||
optimizer.zero_grad()
|
||||
outputs = model(noise, input_ids, attention_mask, map_size)
|
||||
|
||||
print(torch.argmax(torch.softmax(outputs, dim=1), dim=1))
|
||||
# print(sampled[0, :, :, 1])
|
||||
|
||||
# 构建拓扑图
|
||||
# with torch.no_grad():
|
||||
# pred_graphs = build_topology_graph(outputs.argmax(1))
|
||||
# ref_graphs = build_topology_graph(target)
|
||||
|
||||
# 计算损失
|
||||
loss = criterion(
|
||||
outputs, # 调整为 [BS, C, H, W]
|
||||
target
|
||||
)
|
||||
|
||||
# 反向传播
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
tqdm.write(f"[INFO {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Epoch: {epoch} | loss: {total_loss:.6f} | lr: {(optimizer.param_groups[0]['lr']):.6f}")
|
||||
|
||||
# 学习率调整
|
||||
scheduler.step()
|
||||
|
||||
print("Train ended.")
|
||||
|
||||
torch.save({
|
||||
"model_state": model.state_dict(),
|
||||
"optimizer_state": optimizer.state_dict(),
|
||||
}, f"result/ginka.pth")
|
||||
|
||||
if __name__ == "__main__":
|
||||
torch.set_num_threads(8)
|
||||
train()
|
||||
2673
minamo-dataset.json
Normal file
2673
minamo-dataset.json
Normal file
File diff suppressed because it is too large
Load Diff
1
minamo-eval.json
Normal file
1
minamo-eval.json
Normal file
File diff suppressed because one or more lines are too long
29
minamo/dataset.py
Normal file
29
minamo/dataset.py
Normal file
@ -0,0 +1,29 @@
|
||||
import json
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
|
||||
def load_data(path: str):
|
||||
with open(path, 'r', encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
data_list = []
|
||||
for value in data["data"].values():
|
||||
data_list.append(value)
|
||||
|
||||
return data_list
|
||||
|
||||
class MinamoDataset(Dataset):
|
||||
def __init__(self, data_path: str):
|
||||
self.data = load_data(data_path) # 自定义数据加载函数
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.data[idx]
|
||||
return (
|
||||
torch.LongTensor(item['map1']),
|
||||
torch.LongTensor(item['map2']),
|
||||
torch.FloatTensor([item['visionSimilarity']]),
|
||||
torch.FloatTensor([item['topoSimilarity']])
|
||||
)
|
||||
14
minamo/model/loss.py
Normal file
14
minamo/model/loss.py
Normal file
@ -0,0 +1,14 @@
|
||||
import torch.nn as nn
|
||||
|
||||
class MinamoLoss(nn.Module):
|
||||
def __init__(self, vision_weight=0.4, topo_weight=0.6):
|
||||
super().__init__()
|
||||
self.vision_weight = vision_weight
|
||||
self.topo_weight = topo_weight
|
||||
self.mse = nn.MSELoss()
|
||||
|
||||
def forward(self, vis_pred, topo_pred, vis_true, topo_true):
|
||||
# print(vis_pred[0].item(), topo_pred[0].item(), vis_true[0].item(), topo_true[0].item())
|
||||
vis_loss = self.mse(vis_pred, vis_true)
|
||||
topo_loss = self.mse(topo_pred, topo_true)
|
||||
return self.vision_weight * vis_loss + self.topo_weight * topo_loss
|
||||
121
minamo/model/model.py
Normal file
121
minamo/model/model.py
Normal file
@ -0,0 +1,121 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
class DualAttention(nn.Module):
|
||||
def __init__(self, in_channels):
|
||||
super().__init__()
|
||||
# 空间注意力
|
||||
self.spatial = nn.Sequential(
|
||||
nn.Conv2d(in_channels, 1, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
# 通道注意力
|
||||
self.channel = nn.Sequential(
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
nn.Conv2d(in_channels, in_channels//8, 1),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(in_channels//8, in_channels, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return x * self.spatial(x) + x * self.channel(x)
|
||||
|
||||
class DirectionalAttention(nn.Module):
|
||||
def __init__(self, kernel_size=7):
|
||||
super().__init__()
|
||||
self.direction_convs = nn.ModuleDict({
|
||||
dir: nn.Conv2d(1, 1, kernel_size, padding=kernel_size//2,
|
||||
padding_mode='replicate')
|
||||
for dir in ['h', 'v', 'd1', 'd2']
|
||||
})
|
||||
|
||||
def forward(self, x):
|
||||
B, C, H, W = x.shape
|
||||
# 各方向特征
|
||||
h_att = self.direction_convs['h'](x.mean(1, keepdim=True))
|
||||
v_att = self.direction_convs['v'](x.mean(1, keepdim=True))
|
||||
d1_att = self.direction_convs['d1'](x.mean(1, keepdim=True))
|
||||
d2_att = self.direction_convs['d2'](x.mean(1, keepdim=True))
|
||||
|
||||
# 动态融合
|
||||
combined = torch.stack([h_att, v_att, d1_att, d2_att], dim=1) # [B,4,1,H,W]
|
||||
att_weights = F.softmax(combined.mean([3,4]), dim=1) # [B,4]
|
||||
return x * (combined * att_weights.unsqueeze(-1).unsqueeze(-1)).sum(1)
|
||||
|
||||
class MinamoModel(nn.Module):
|
||||
def __init__(self, num_tile_types, embedding_dim=64, conv_channels=256):
|
||||
super().__init__()
|
||||
# 嵌入层处理不同图块类型
|
||||
self.embedding = nn.Embedding(num_tile_types, embedding_dim)
|
||||
|
||||
# 共享特征提取的卷积层
|
||||
self.conv_layers = nn.Sequential(
|
||||
nn.Conv2d(embedding_dim, conv_channels, 3, padding=1),
|
||||
DualAttention(conv_channels),
|
||||
DirectionalAttention(),
|
||||
nn.ReLU(),
|
||||
nn.BatchNorm2d(conv_channels),
|
||||
|
||||
nn.Conv2d(conv_channels, conv_channels*2, 3, padding=1),
|
||||
DualAttention(conv_channels*2),
|
||||
DirectionalAttention(),
|
||||
nn.ReLU(),
|
||||
nn.BatchNorm2d(conv_channels*2),
|
||||
|
||||
nn.Conv2d(conv_channels*2, conv_channels*4, 3, padding=1),
|
||||
DualAttention(conv_channels*4),
|
||||
DirectionalAttention(),
|
||||
nn.ReLU(),
|
||||
nn.BatchNorm2d(conv_channels*4),
|
||||
)
|
||||
|
||||
# 自适应池化处理任意尺寸
|
||||
self.pool = nn.ModuleDict({
|
||||
'avg': nn.AdaptiveAvgPool2d((1,1)),
|
||||
'max': nn.AdaptiveMaxPool2d((1,1))
|
||||
})
|
||||
|
||||
# 多任务预测头
|
||||
head_dim = conv_channels * 4 * 2 * 4 # 2个池化,四个交互项
|
||||
self.vision_head = nn.Sequential(
|
||||
nn.Linear(head_dim, 512),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(0.3),
|
||||
nn.Linear(512, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
self.topo_head = nn.Sequential(
|
||||
nn.Linear(head_dim, 512),
|
||||
nn.GELU(),
|
||||
nn.Dropout(0.3),
|
||||
nn.Linear(512, 256),
|
||||
nn.GELU(),
|
||||
nn.Linear(256, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
|
||||
def forward(self, map1, map2):
|
||||
# 增强特征提取
|
||||
def process_map(x):
|
||||
x = self.embedding(x).permute(0,3,1,2)
|
||||
x = self.conv_layers(x)
|
||||
return torch.cat([
|
||||
self.pool['avg'](x),
|
||||
self.pool['max'](x)
|
||||
], dim=1).flatten(1)
|
||||
|
||||
f1 = process_map(map1)
|
||||
f2 = process_map(map2)
|
||||
|
||||
# 特征融合
|
||||
combined = torch.cat([f1, f2, f1-f2, f1*f2], dim=1) # [B, 256]
|
||||
|
||||
# 多任务输出
|
||||
vision_sim = self.vision_head(combined)
|
||||
topo_sim = self.topo_head(combined)
|
||||
|
||||
return vision_sim, topo_sim
|
||||
131
minamo/train.py
Normal file
131
minamo/train.py
Normal file
@ -0,0 +1,131 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
import torch
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
from .model.model import MinamoModel
|
||||
from .model.loss import MinamoLoss
|
||||
from .dataset import MinamoDataset
|
||||
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
os.makedirs("result", exist_ok=True)
|
||||
|
||||
epochs = 100
|
||||
|
||||
def collate_fn(batch):
|
||||
"""动态处理不同尺寸地图的批处理"""
|
||||
map1_batch = [item[0] for item in batch]
|
||||
map2_batch = [item[1] for item in batch]
|
||||
vis_sim = torch.cat([item[2] for item in batch])
|
||||
topo_sim = torch.cat([item[3] for item in batch])
|
||||
|
||||
# 保持批次内地图尺寸一致(根据问题描述)
|
||||
assert all(m.shape == map1_batch[0].shape for m in map1_batch), \
|
||||
"对比地图必须尺寸相同"
|
||||
|
||||
return (
|
||||
torch.stack(map1_batch), # (B, H, W)
|
||||
torch.stack(map2_batch), # (B, H, W)
|
||||
vis_sim,
|
||||
topo_sim
|
||||
)
|
||||
|
||||
def train():
|
||||
print(f"Using {"cuda" if torch.cuda.is_available() else "cpu"} to train model.")
|
||||
model = MinamoModel(32)
|
||||
model.to(device)
|
||||
|
||||
# 准备数据集
|
||||
dataset = MinamoDataset("F:/github-ai/ginka-generator/minamo-dataset.json")
|
||||
val_dataset = MinamoDataset("F:/github-ai/ginka-generator/minamo-eval.json")
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=32,
|
||||
shuffle=True
|
||||
)
|
||||
val_loader = DataLoader(
|
||||
val_dataset,
|
||||
batch_size=32,
|
||||
shuffle=True
|
||||
)
|
||||
|
||||
# 设定优化器与调度器
|
||||
optimizer = optim.AdamW(model.parameters(), lr=3e-5, weight_decay=1e-4)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
|
||||
criterion = MinamoLoss()
|
||||
|
||||
# 开始训练
|
||||
for epoch in tqdm(range(epochs)):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
|
||||
for batch in dataloader:
|
||||
# 数据迁移到设备
|
||||
map1, map2, vision_simi, topo_simi = batch
|
||||
map1 = map1.to(device) # 转为 [B, C, H, W]
|
||||
map2 = map2.to(device)
|
||||
topo_simi = topo_simi.to(device)
|
||||
vision_simi = vision_simi.to(device)
|
||||
|
||||
# print(map1.shape, map2.shape)
|
||||
|
||||
# 前向传播
|
||||
optimizer.zero_grad()
|
||||
vision_pred, topo_pred = model(map1, map2)
|
||||
|
||||
# 计算损失
|
||||
loss = criterion(vision_pred, topo_pred, vision_simi, topo_simi)
|
||||
|
||||
# 反向传播
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
total_norm = 0
|
||||
for p in model.parameters():
|
||||
if p.grad is not None:
|
||||
param_norm = p.grad.detach().data.norm(2)
|
||||
total_norm += param_norm.item() ** 2
|
||||
total_norm = total_norm ** 0.5
|
||||
# tqdm.write(f"Gradient Norm: {total_norm:.4f}") # 正常应保持在1~100之间
|
||||
|
||||
ave_loss = total_loss / len(dataloader)
|
||||
tqdm.write(f"[INFO {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Epoch: {epoch} | loss: {ave_loss:.6f} | lr: {(optimizer.param_groups[0]['lr']):.6f}")
|
||||
|
||||
# 学习率调整
|
||||
scheduler.step()
|
||||
|
||||
# 每十轮推理一次验证集
|
||||
if (epoch + 1) % 10 == 0:
|
||||
model.eval()
|
||||
val_loss = 0
|
||||
with torch.no_grad():
|
||||
for val_batch in val_loader:
|
||||
map1_val, map2_val, vision_simi_val, topo_simi_val = val_batch
|
||||
map1_val = map1_val.to(device)
|
||||
map2_val = map2_val.to(device)
|
||||
vision_simi_val = vision_simi_val.to(device)
|
||||
topo_simi_val = topo_simi_val.to(device)
|
||||
|
||||
vision_pred_val, topo_pred_val = model(map1_val, map2_val)
|
||||
loss_val = criterion(
|
||||
vision_pred_val, topo_pred_val,
|
||||
vision_simi_val, topo_simi_val
|
||||
)
|
||||
val_loss += loss_val.item()
|
||||
|
||||
avg_val_loss = val_loss / len(val_loader)
|
||||
tqdm.write(f"[INFO {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Validation :: loss: {avg_val_loss:.6f}")
|
||||
|
||||
|
||||
print("Train ended.")
|
||||
|
||||
torch.save({
|
||||
"model_state": model.state_dict(),
|
||||
"optimizer_state": optimizer.state_dict(),
|
||||
}, "result/minamo.pth")
|
||||
|
||||
if __name__ == "__main__":
|
||||
torch.set_num_threads(2)
|
||||
train()
|
||||
56
shared/attention.py
Normal file
56
shared/attention.py
Normal file
@ -0,0 +1,56 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
class ChannelAttention(nn.Module):
|
||||
"""通道注意力模块"""
|
||||
def __init__(self, channels, reduction=8):
|
||||
super().__init__()
|
||||
# 通道注意力
|
||||
self.channel_att = nn.Sequential(
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
nn.Conv2d(channels, channels//reduction, 1),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(channels//reduction, channels, 1),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
# 通道注意力
|
||||
c_att = self.channel_att(x)
|
||||
x = x * c_att
|
||||
return x
|
||||
|
||||
class SpatialAttention(nn.Module):
|
||||
"""空间注意力模块"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# 空间注意力
|
||||
self.spatial_att = nn.Sequential(
|
||||
nn.Conv2d(2, 1, 7, padding=3),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
# 空间注意力
|
||||
max_pool = torch.max(x, dim=1, keepdim=True)[0]
|
||||
avg_pool = torch.mean(x, dim=1, keepdim=True)
|
||||
s_att = self.spatial_att(torch.cat([max_pool, avg_pool], dim=1))
|
||||
return x * s_att
|
||||
|
||||
class CBAM(nn.Module):
|
||||
"""通道与空间注意力结合"""
|
||||
def __init__(self, channels, reduction=8):
|
||||
super().__init__()
|
||||
# 通道注意力
|
||||
self.channel_att = ChannelAttention(channels, reduction)
|
||||
# 空间注意力
|
||||
self.spatial_att = SpatialAttention()
|
||||
|
||||
def forward(self, x):
|
||||
# 通道注意力
|
||||
c_att = self.channel_att(x)
|
||||
x = x * c_att
|
||||
|
||||
# 空间注意力
|
||||
s_att = self.spatial_att(x)
|
||||
return x * s_att
|
||||
Loading…
Reference in New Issue
Block a user