├── 16x16-overworld.png
├── NOTES
├── README
├── formosa.tmx
├── gpl-3.0.txt
├── npc
├── __init__.py
└── pirate
│ ├── __init__.py
│ └── actions.py
├── pathfinding
├── __init__.py
└── astar.py
├── pygoap
├── README
├── __init__.py
├── actions.py
├── actionstates.py
├── agent.py
├── blackboard.py
├── context.py
├── environment.py
├── environment2d.py
├── goals.py
├── memory.py
├── planning.py
├── precepts.py
└── tiledenvironment.py
├── test.py
├── tmxloader.py
└── tutorial.py
/16x16-overworld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitcraft/pygoap/9915458037a080cf95df263f7df4c7562da888fd/16x16-overworld.png
--------------------------------------------------------------------------------
/NOTES:
--------------------------------------------------------------------------------
1 | for goap 3
2 |
3 | THIS IS MOSTLY NOTES FOR MYSELF
4 |
5 |
6 | planner
7 |
8 | planner should be able to reasonably predict the cost of repeating actions
9 |
10 | agents:
11 | in an rpg game, the player can expect to talk to NPC's and and to gather
12 | information about quests, items, other npc's, etc.
13 |
14 | i think that goap can become an engine of sorts, dictating every action in the
15 | game. basic play elements can be emulated through goap and would not have to
16 | be explicitly coded into the game. take simple "hello" meet/greet actions with
17 | an npc:
18 | npc will have a insatiable desire to talk to a player
19 | they will have a different thing to say depending on mood/state of player
20 |
21 | a side effect of this will that npc's could possibly become more lifelike as
22 | they can move around the game world to satisfy goals. to moderate and control
23 | the npc's and to make the game more enjoyable, npc's can have goals that are
24 | only relevant at certain times of day or days of the week.
25 |
26 | in the harvest moon series for example, the nps do have certain schedules that
27 | they will loosely follow. this makes the game play predictable once their
28 | simple schedule is learned. imo, giving the npc's too much freedom to act will
29 | make the game world seem more random, and potentially frustrating to play.
30 |
31 | tying the speech system to goap agents could make the gameplay more immersive
32 | by allowing the player to ask unscripted questions. while developing a system
33 | that perfectly synthesizes english is not a viable option, giving the player
34 | the option to ask canned questions, unrelated to the quest or story, with the
35 | ability to choose specific parts of the question is a definite cool thing.
36 |
37 | for example, the player might ask an npc "have you seen gary?". the goap
38 | agent can then search it's memories of gary and give a response to the player.
39 | because this would be based on the agent's memory, and not some canned
40 | response, it will simultaneously make the make more immersive and relieve the
41 | game designers and writers the burden to creating dialog.
42 |
43 | a frustrating aspect of some games is dealing with faction alliances. for
44 | example, in some games, killing or doing some negative action against a member
45 | of a faction will cause all members of that faction to instantly become hostile
46 | toward the player. this is unrealistic in situations where the information
47 | that our your hostility could not have reached the other party.
48 |
49 | simulating the spread of information could also be simulated in goap by
50 | creating goals that one agent wants to tell other agents things that he saw the
51 | player do. for example, they may be neutral npc's that will gossip with other
52 | npc's in distant towns. or, members of one faction may use radio, phones,
53 | letters, messengers, etc to tell other faction members about the player.
54 |
55 | this too could be emulated with goap, and would not have to be explicitly
56 | programmed.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | pygoap v.3
2 | requires python 2.x
3 |
4 | PyGoap is a small library for designing AI in python. The basic library is based off of a well-known idea of using graph searches to to create realistic agents in real-time. Behavior is determined in real-time and is extremely open ended and well suited for emergent behaviors.
5 |
6 | Agents, or npcs, can be programmed very simply with an easy to understand api. Check the 'npcs' folder and read through actions.py to get an idea of how it works.
7 |
8 | This library is not complete, but is working as-is. The demo gives you a simple diagram of the game world and the debugging information near the bottom lets you know what is going on 'in the heads' of the agents. Note: the demo requires pygame.
9 |
10 |
11 |
12 | I've build AI Agents around the concept of Actions and ActionBuilders.
13 |
14 | Actions can be subclassed from a few different action types.
15 | ActionBuilders will search a blackboard and return a list of actions that can be performed.
16 |
17 |
18 |
19 | If you are unfamiliar with GOAP, I invite you to do some research online, it may help you to understand this module.
20 |
21 |
22 |
23 | you can watch it work by running "test.py".
24 |
25 |
26 | many thanks to opengameart.org for providing and hosting the tileset used in the project. tiles for the map can be found at:
27 | http://opengameart.org/content/worldmapoverworld-tileset
28 | under the CC-BY 3.0 license.
29 |
30 | have fun.
--------------------------------------------------------------------------------
/formosa.tmx:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/gpl-3.0.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/npc/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/npc/pirate/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitcraft/pygoap/9915458037a080cf95df263f7df4c7562da888fd/npc/pirate/__init__.py
--------------------------------------------------------------------------------
/npc/pirate/actions.py:
--------------------------------------------------------------------------------
1 | """
2 | This is an example module for programming actions for a pyGOAP agent.
3 |
4 | The module must contain a list called "exported_actions". This list should
5 | contain any classes that you would like to add to the planner.
6 |
7 | To make it convenient, I have chosen to add the class to the list after each
8 | declaration, although you may choose another way.
9 | """
10 |
11 | from pygoap.actions import *
12 | from pygoap.goals import *
13 |
14 |
15 | def get_position(entity, memory):
16 | """
17 | Return the position of [entity] according to memory.
18 | """
19 | for pct in memory.of_class(PositionPrecept):
20 | if pct.entity is entity:
21 | return pct.position
22 |
23 |
24 | class LookAction(CalledOnceContext):
25 | def enter(self):
26 | self.parent.environment.look(self.parent)
27 |
28 |
29 | class MoveAction(ActionContext):
30 | def enter(self):
31 | self.path = self.parent.environment.pathfind(self.startpoint,
32 | self.endpoint)
33 |
34 | # remove the first node, which is the starting node
35 | self.path.pop()
36 |
37 | def update(self, time):
38 | if self.path:
39 | pos = self.path.pop()
40 | self.parent.environment.set_position(self.parent,
41 | (self.parent.environment, pos))
42 |
43 | if not self.path:
44 | self.finish()
45 |
46 | def setStartpoint(self, pos):
47 | self.startpoint = pos
48 |
49 | def setEndpoint(self, pos):
50 | self.endpoint = pos
51 |
52 |
53 | class PickupAction(CalledOnceContext):
54 | """
55 | take an object from the environment and place it into your inventory
56 | """
57 | pass
58 |
59 |
60 | class DrinkRumAction(ActionContext):
61 | def enter(self):
62 | print "drinking!"
63 | self.drunkness = 1
64 |
65 | def update(self, time):
66 | self.drunkness += 1
67 | if self.drunkness >= 3:
68 | self.parent.set_condition('drunk', True)
69 | self.finish()
70 |
71 | exported_actions = []
72 |
73 |
74 | ### ACTION BUILDERS
75 | ###
76 |
77 | class move_to_entity(ActionBuilder):
78 | """
79 | return a list of action that this caller is able to move with
80 | """
81 |
82 | def get_actions(self, caller, memory):
83 | here = get_position(caller, memory)
84 | visited = []
85 |
86 | for pct in memory.of_class(PositionPrecept):
87 | if pct.entity is caller or pct.position in visited:
88 | continue
89 |
90 | visited.append(pct.position)
91 |
92 | action = MoveAction(caller)
93 | action.setStartpoint(here)
94 | action.setEndpoint(pct.position[1])
95 | action.effects.append(PositionGoal(caller, pct.position))
96 | yield action
97 |
98 | exported_actions.append(move_to_entity)
99 |
100 |
101 | class pickup(ActionBuilder):
102 | def get_actions(self, caller, memory):
103 | """
104 | return list of actions that will pickup an item at caller's position
105 | """
106 | here = get_position(caller, memory)
107 |
108 | for pct in memory.of_class(PositionPrecept):
109 | if here == pct.position and pct.entity is not caller:
110 | action = PickupAction(caller)
111 | action.effects.append(HasItemGoal(pct.entity))
112 | yield action
113 |
114 | exported_actions.append(pickup)
115 |
116 |
117 | class drink_rum(ActionBuilder):
118 | """
119 | drink rum that is in caller's inventory
120 | """
121 | def get_actions(self, caller, memory):
122 | for pct in memory.of_class(PositionPrecept):
123 | #print "looking for rum", pct
124 | if pct.position[0] == 'self' and pct.entity.name == "rum":
125 | action = DrinkRumAction(caller)
126 | action.effects.append(SimpleGoal(is_drunk=True))
127 | #action.effects.append(EvalGoal("charisma = charisma + 10"))
128 | yield action
129 |
130 |
131 | exported_actions.append(drink_rum)
132 |
133 |
134 | class look(ActionBuilder):
135 | def get_actions(self, caller, memory):
136 | action = LookAction(caller)
137 | action.effects.append(SimpleGoal(aware=True))
138 | yield action
139 |
140 |
141 | exported_actions.append(look)
142 |
--------------------------------------------------------------------------------
/pathfinding/__init__.py:
--------------------------------------------------------------------------------
1 | from astar import Astar
2 |
--------------------------------------------------------------------------------
/pathfinding/astar.py:
--------------------------------------------------------------------------------
1 | from heapq import heappush, heappop, heappushpop, heapify
2 | from collections import defaultdict
3 |
4 | class Astar(object):
5 | pass
6 |
7 |
8 | class Node(object):
9 | __slots__ = ['parent', 'x', 'y', 'g', 'h', 'f', 'is_closed']
10 |
11 | def __init__(self, pos=(0,0)):
12 | self.x, self.y = pos
13 | self.parent = None
14 | self.g = 0
15 | self.h = 0
16 | self.is_closed = 0
17 |
18 | def __eq__(self, other):
19 | try:
20 | return (self.x == other.x) and (self.y == other.y)
21 | except AttributeError:
22 | return False
23 |
24 | def __repr__(self):
25 | return "".format(self.x, self.y)
26 |
27 | def getSurrounding(node):
28 | return ((node.x-1, node.y-1), (node.x, node.y-1), (node.x+1, node.y-1), \
29 | (node.x-1, node.y), (node.x+1, node.y), \
30 | (node.x-1, node.y+1), (node.x, node.y+1), (node.x+1, node.y+1))
31 |
32 | def dist(start, finish):
33 | return abs(finish.x - start.x) + abs(finish.y - start.y)
34 |
35 | def calcG(node):
36 | score=0
37 | score += node.g
38 | while not node.parent == None:
39 | node = node.parent
40 | score += node.g
41 | return score
42 |
43 |
44 | def calcH(node, finish):
45 | # somehow factor in the cost of other nodes
46 | return abs(finish.x - node.x) + abs(finish.y - node.y)
47 |
48 |
49 | def search(start, finish, factory):
50 | """perform basic a* search on a 2d map.
51 |
52 | Args:
53 | start: tuple that defines the starting position
54 | finish: tuple that defined the finish
55 | factory: function that will return a Node object from a position
56 |
57 | Factory can return None, which means the area is not passable.
58 |
59 | """
60 |
61 | finishNode = factory(finish)
62 | startNode = factory(start)
63 | startNode.h = calcH(startNode, finishNode)
64 |
65 | # used to locate nodes in the heap and modify their f scores
66 | heapIndex = {}
67 | entry = [startNode.g + startNode.h, startNode]
68 | heapIndex[startNode] = entry
69 |
70 | openlist = [entry]
71 |
72 | nodeHash = {}
73 | nodeHash[start] = startNode
74 |
75 | while openlist:
76 | try:
77 | f, keyNode = heappop(openlist)
78 | while keyNode == None:
79 | f, keyNode = heappop(openlist)
80 | except IndexError:
81 | break
82 | else:
83 | del heapIndex[keyNode]
84 |
85 | if keyNode == finishNode:
86 | path = [(keyNode.x, keyNode.y)]
87 | while not keyNode.parent == None:
88 | keyNode = keyNode.parent
89 | path.append((keyNode.x, keyNode.y))
90 | return path
91 |
92 | keyNode.is_closed = 1
93 |
94 | for neighbor in getSurrounding(keyNode):
95 | try:
96 | node = nodeHash[neighbor]
97 | except KeyError:
98 | node = factory(neighbor)
99 | if node:
100 | nodeHash[neighbor] = node
101 | score = keyNode.g + dist(keyNode, node)
102 | node.parent = keyNode
103 | node.g = score
104 | node.h = calcH(node, finishNode)
105 | entry = [node.g + node.h, node]
106 | heapIndex[node] = entry
107 | heappush(openlist, entry)
108 | else:
109 | if not node.is_closed:
110 | score = keyNode.g + dist(keyNode, node)
111 | if score < node.g:
112 | node.parent = keyNode
113 | node.g = score
114 | entry = heapIndex.pop(node)
115 | entry[1] = None
116 | newentry = [node.g + node.h, node]
117 | heapIndex[node] = newentry
118 | heappush(openlist, newentry)
119 |
120 | return []
121 |
122 |
123 | def search_test(tests=1000):
124 | area = [[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
125 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
126 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
127 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
128 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
129 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
130 | [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
131 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
132 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
133 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]]
134 |
135 | def factory((x, y)):
136 | if x < 0 or y < 0:
137 | return None
138 |
139 | try:
140 | if area[y][x] == 0:
141 | node = Node((x, y))
142 | return node
143 | else:
144 | return None
145 | except IndexError:
146 | return None
147 |
148 | return search((0,0), (5,9), factory)
149 |
150 |
151 | if __name__ == "__main__":
152 | print search_test()
153 |
154 |
--------------------------------------------------------------------------------
/pygoap/README:
--------------------------------------------------------------------------------
1 | how to handle ai? multiprocessing to get around the GIL in CPython.
2 | why not threads? because we are CPU restricted, not IO.
3 |
4 | memory managers and blackboards should be related.
5 | this will allow for expectations, since a memory can be simulated in the future
6 |
7 | memory:
8 | should be a heap
9 | memory added will be wrapped with a counter
10 | every time a memory is fetched, the counter will be added
11 | eventually, memories not being used will be removed
12 | and the counters will be reset
13 |
14 | memories should be a tree
15 | if a memory is being added that is similar to an existing memory,
16 | then the existing memory will be updated, rather than replaced.
17 |
18 | since goals and prereqs share a common function, "valid", it makes sene to
19 | make them subclasses of a common class.
20 |
21 | looking at actions.csv, it is easy to see that the behaviour of the agent will
22 | be largely dependent on how well the action map is defined. with the little
23 | pirate demo, it is not difficult to model his behaviour, but with larger, more
24 | complex agents, it could quickly become a huge task to write and verify the
25 | action map.
26 |
27 | with some extra steps, i would like to make it possible that the agent can
28 | infer the prereq's to a goal through clues provided within the objects that the
29 | agent interacts with, rather than defining them within the class. i can foresee
30 | a performance penality for this, but that could be offset by constructing
31 | training environments for the agent and then storing the action map that the
32 | agent creates during training.
33 |
34 | the planner:
35 | GOAP calls for a heuristic to be used to find the optimal solution and to reduce
36 | the number of checks made. in a physical environment where a star is used,
37 | it makes sense to just find a vector from the current searched node to the
38 | goal, but in action planning, there is no spatial dimension where a simple
39 | solution like that can be used.
40 |
41 | without a heuristic, a* is just a tree search. the heuristic will increase the
42 | efficiency of the planner and possibly give more consistent results. it can
43 | also be used to guide an agents behavior by manipulating some values.
44 |
45 | for now, while testing and building the library, the h value will not be used.
46 | when the library is more complete, it would make sense to build a complete
47 | agent, then construct a set of artificial scenarios to train the agent.
48 | based on data from the scenarios, it could be possible to hardcode the h
49 | values. the planner could then be optimized for certain scenarios.
50 |
51 | This module contains the most commonly used parts of the system. Classes that
52 | have many related sibling classes are in other modules.
53 |
54 |
55 | since planning is done on the blackboard and i would like agents to be able to
56 | make guesses, or plans about other agents, then agents will have to somehow be
57 | able to be stored on and manipulated on a blackboard. this may meant that
58 | agents and precepts will be the same thing
59 |
60 | 1/15/12:
61 | overhauled the concepts of goals, prereqs, and effects and rolled them into
62 | one class. with the new system of instanced actions, it makes design sense to
63 | consolidate them, since they all have complimentary functionality. from a
64 | performance standpoint, it may make sense to keep the separate, but this way is
65 | much easier to conceptualize in your mind, and i am not making an system that
66 | is performance sensitive....this is python.
67 |
68 | simplify holding/inventory:
69 | an objects location should always be a tuple of:
70 | ( holding object, position )
71 |
72 | this will make position very simple to sort. the position in the tuple should
73 | be a value the that holding object can make sense of, for example, an
74 | environment might expect a zone, and (x,y), while an agent would want an index
75 | number in their inventory.
76 |
77 | a side effect will be that location goals and holding goals can be consolidated
78 | into one function.
79 |
80 | new precepts may render a current plan invalid. to account for this, an agent
81 | will re-plan every time it receives a precept. a better way would be to tag a
82 | type of precept and then if a new one that directly relates to the plan arrives
83 | then re-plan.
84 |
--------------------------------------------------------------------------------
/pygoap/__init__.py:
--------------------------------------------------------------------------------
1 | from actions import ActionContext
2 |
--------------------------------------------------------------------------------
/pygoap/actions.py:
--------------------------------------------------------------------------------
1 | """
2 | These are the building blocks for creating pyGOAP agents that are able to
3 | interact with their environment in a meaningful way.
4 |
5 | When actions are updated, they can return a precept for the environment to
6 | process. The action can emit a sound, sight, or anything else for other
7 | objects to consume.
8 |
9 | These classes will be known to an agent, and chosen by the planner as a means
10 | to satisfy the current goal. They will be instanced and the agent will
11 | execute the action in some way, one after another.
12 |
13 | Actions need to be split into ActionInstances and ActionBuilders.
14 |
15 | An ActionInstance's job is to work in a planner and to carry out actions.
16 | A ActionBuilder's job is to query the parent and return a list of suitable
17 | actions for the memory.
18 | """
19 |
20 | from actionstates import *
21 | import sys
22 |
23 |
24 |
25 | test_fail_msg = "some goal is returning None on a test, this is a bug."
26 |
27 | class ActionBuilder(object):
28 | """
29 | ActionBuilders examine a blackboard and return a list of actions that can
30 | be successfully completed at the time.
31 |
32 | The actions that are returned will be assumed to be valid and will not be
33 | tested. Please make sure that the actions are valid.
34 | """
35 |
36 | def __call__(self, parent, memory):
37 | return self.get_actions(parent, memory)
38 |
39 | def get_actions(self, parent, memory):
40 | """
41 | Return a list of actions
42 | """
43 | raise NotImplementedError
44 |
45 | def __repr__(self):
46 | return "".format(self.__class__.__name__)
47 |
48 |
49 | class ActionContext(object):
50 | """
51 | Context where actions take place.
52 | """
53 |
54 | def __init__(self, parent, **kwargs):
55 | self.parent = parent
56 | self.state = ACTIONSTATE_NOT_STARTED
57 | self.prereqs = []
58 | self.effects = []
59 | self.costs = {}
60 | self.__dict__.update(kwargs)
61 |
62 | def __enter__(self):
63 | """
64 | Please do not override this method. Use enter instead.
65 | """
66 | self.state = ACTIONSTATE_RUNNING
67 | self.enter()
68 | return self
69 |
70 | def __exit__(self, *exc):
71 | """
72 | Please do not override this method. Use exit instead.
73 | """
74 | if self.state == ACTIONSTATE_RUNNING:
75 | self.state = ACTIONSTATE_FINISHED
76 | if not self.state == ACTIONSTATE_ABORTED:
77 | self.exit()
78 | return False
79 |
80 | def enter(self):
81 | """
82 | This method will be called after this context becomes active
83 | """
84 | pass
85 |
86 | def exit(self):
87 | """
88 | This method will be called after this context become inactive
89 | """
90 | pass
91 |
92 | def update(self, time):
93 | """
94 | This method will be called periodically by the environment.
95 | """
96 | pass
97 |
98 | def finish(self):
99 | """
100 | Call this method when context is no longer needed or is finished.
101 | Do not override. Handle cleanup in exit instead.
102 | """
103 | self.state = ACTIONSTATE_FINISHED
104 |
105 | def fail(self):
106 | """
107 | Call this method if the context is not able to complete
108 | Do not override. Handle cleanup in exit instead.
109 | """
110 | self.state = ACTIONSTATE_FAILED
111 |
112 | def abort(self):
113 | """
114 | Call this method to stop this context without cleaning it up
115 | Do not override. Handle cleanup in exit instead.
116 | """
117 | self.state = ACTIONSTATE_ABORTED
118 |
119 | def test(self, memory=None):
120 | """
121 | Determine whether or not this context is able to start (begin())
122 |
123 | return a float from 0-1 that describes how valid this action is.
124 |
125 | validity of an action is a measurement of how effective the action will
126 | be if it is completed successfully.
127 |
128 | if any of the prereqs are not partially valid ( >0 ) then will return 0
129 |
130 | for many actions a simple 0 or 1 will work. for actions which
131 | modify numerical values, it may be useful to return a fractional value.
132 | """
133 |
134 | if not self.prereqs: return 1.0
135 |
136 | if memory is None: raise Exception
137 | values = ( i.test(memory) for i in self.prereqs )
138 |
139 | try:
140 | return float(sum(values)) / len(self.prereqs)
141 | except TypeError:
142 | print zip(values, self.prereqs)
143 | print test_fail_msg
144 | sys.exit(1)
145 |
146 | def touch(self, memory=None):
147 | """
148 | Call after the planning phase is complete.
149 | """
150 | if memory is None:
151 | memory = self.parent.memory
152 | [ i.touch(memory) for i in self.effects ]
153 |
154 | def __repr__(self):
155 | return ''.format(self.__class__.__name__)
156 |
157 |
158 | class CalledOnceContext(ActionContext):
159 | """
160 | Is finished immediately when started.
161 | """
162 |
163 | def __enter__(self):
164 | if self.test() == 1.0:
165 | super(CalledOnceContext, self).__enter__()
166 | super(CalledOnceContext, self).__exit__()
167 | else:
168 | self.fail()
169 |
170 |
--------------------------------------------------------------------------------
/pygoap/actionstates.py:
--------------------------------------------------------------------------------
1 | ACTIONSTATE_NOT_STARTED = 0
2 | ACTIONSTATE_FINISHED = 1
3 | ACTIONSTATE_RUNNING = 2
4 | ACTIONSTATE_PAUSED = 3
5 | ACTIONSTATE_ABORTED = 4
6 | ACTIONSTATE_FAILED = 5
7 |
--------------------------------------------------------------------------------
/pygoap/agent.py:
--------------------------------------------------------------------------------
1 | from environment import ObjectBase
2 | from planning import plan
3 | from actions import ActionContext
4 | from memory import MemoryManager
5 | from actionstates import *
6 | from precepts import *
7 | import logging
8 |
9 | debug = logging.debug
10 |
11 |
12 |
13 | NullAction = ActionContext(None)
14 | NullAction.__enter__()
15 | NullAction.__exit__()
16 |
17 | # required to reduce memory usage
18 | def time_filter(precept):
19 | return None if isinstance(precept, TimePrecept) else precept
20 |
21 | class GoapAgent(ObjectBase):
22 | """
23 | AI Agent
24 |
25 | inventories will be implemented using precepts and a list.
26 | currently, only one action running concurrently is supported.
27 | """
28 |
29 | # this will set this class to listen for this type of precept
30 | # not implemented yet
31 | interested = []
32 | idle_timeout = 30
33 |
34 | def __init__(self, name=None):
35 | super(GoapAgent, self).__init__(name)
36 | self.memory = MemoryManager()
37 | self.planner = plan
38 |
39 | self.current_goal = None
40 |
41 | self.goals = [] # all goals this instance can use
42 | self.filters = [] # list of methods to use as a filter
43 | self.actions = [] # all actions this npc can perform
44 | self.plan = [] # list of actions to perform
45 | # '-1' will be the action currently used
46 |
47 | # this special filter will prevent time precepts from being stored
48 | self.filters.append(time_filter)
49 |
50 | def add_goal(self, goal):
51 | self.goals.append(goal)
52 |
53 | def remove_goal(self, goal):
54 | self.goals.remove(goal)
55 |
56 | def add_action(self, action):
57 | self.actions.append(action)
58 |
59 | def remove_action(self, action):
60 | self.actions.remove(action)
61 |
62 | def filter_precept(self, precept):
63 | """
64 | precepts can be put through filters to change them.
65 | this can be used to simulate errors in judgement by the agent.
66 | """
67 | for f in self.filters:
68 | precept = f(precept)
69 | if precept is None:
70 | break
71 |
72 | return precept
73 |
74 | def process(self, precept):
75 | """
76 | used by the environment to feed the agent precepts.
77 | agents can respond by sending back an action to take.
78 | """
79 | precept = self.filter_precept(precept)
80 |
81 | if precept:
82 | debug("[agent] %s recv'd precept %s", self, precept)
83 | self.memory.add(precept)
84 |
85 | if self.next_action is NullAction:
86 | self.replan()
87 | return self.next_action
88 |
89 |
90 | def replan(self):
91 | """
92 | force agent to re-evaluate goals and to formulate a plan
93 | """
94 |
95 | # get the relevancy of each goal according to the state of the agent
96 | s = ( (g.get_relevancy(self.memory), g) for g in self.goals )
97 | s = [ g for g in s if g[0] > 0.0 ]
98 | s.sort(reverse=True)
99 |
100 | debug("[agent] %s has goals %s", self, s)
101 |
102 | start_action = NullAction
103 |
104 | # starting for the most relevant goal, attempt to make a plan
105 | self.plan = []
106 | for score, goal in s:
107 | tentative_plan = self.planner(self, self.actions,
108 | start_action, self.memory, goal)
109 |
110 | if tentative_plan:
111 | tentative_plan.pop()
112 | pretty = list(reversed(tentative_plan[:]))
113 | debug("[agent] %s has planned to %s", self, goal)
114 | debug("[agent] %s has plan %s", self, pretty)
115 | self.plan = tentative_plan
116 | self.current_goal = goal
117 | break
118 |
119 |
120 | # we only support one concurrent action (i'm lazy)
121 | def running_actions(self):
122 | return self.current_action
123 |
124 | @property
125 | def current_action(self):
126 | """
127 | get the current action of the current plan
128 | """
129 |
130 | try:
131 | return self.plan[-1]
132 | except IndexError:
133 | return NullAction
134 |
135 | @property
136 | def next_action(self):
137 | """
138 | if the current action is finished, return the next
139 | otherwise, return the current action
140 | """
141 |
142 | # this action is done
143 | if self.current_action.state == ACTIONSTATE_FINISHED:
144 |
145 | # there are more actions in the queue, so just return the next one
146 | if self.plan:
147 | return self.plan.pop()
148 |
149 | # no more actions, so the plan worked!
150 | else:
151 |
152 | # let the goal do its magic to the memory manager
153 | if self.current_goal:
154 | self.current_goal.touch(self.memory)
155 | self.current_goal = None
156 |
157 | return NullAction
158 |
159 | # this action failed somehow
160 | elif self.current_action.state == ACTIONSTATE_FAILED:
161 | raise Exception, "action failed, don't know what to do now!"
162 |
163 | # our action is still running, just run that
164 | elif self.current_action.state == ACTIONSTATE_RUNNING:
165 | return self.current_action
166 |
--------------------------------------------------------------------------------
/pygoap/blackboard.py:
--------------------------------------------------------------------------------
1 | """
2 | Memories are stored precepts.
3 | """
4 |
5 |
6 |
7 | class MemoryManager(set):
8 | """
9 | Store and manage precepts.
10 |
11 | Shared blackboards violate reality in that multiple agents share the same
12 | thoughts, to extend the metaphor. But, the advantage of this is that in
13 | a real-time simulation, it gives the player the impression that the agents
14 | are able to collaborate in some meaningful way, without a significant
15 | impact in performance.
16 |
17 | That being said, i have chosen to restrict blackboards to a per-agent
18 | basis. This library is meant for RPGs, where the action isn't real-time
19 | and would require a more realistic simulation of intelligence.
20 | """
21 |
22 | def of_class(self, klass):
23 | for i in self:
24 | if isinstance(i, klass):
25 | yield i
26 |
27 |
--------------------------------------------------------------------------------
/pygoap/context.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright 2010, 2011 Leif Theden
3 |
4 |
5 | This file is part of lib2d.
6 |
7 | lib2d is free software: you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation, either version 3 of the License, or
10 | (at your option) any later version.
11 |
12 | lib2d is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License
18 | along with lib2d. If not, see .
19 | """
20 |
21 | import pygame
22 | from pygame.locals import *
23 |
24 |
25 | class Context(object):
26 | """
27 | Contexts are used anywhere a handler uses a stack to manage information.
28 | Currently are used in:
29 | Context Driver for the game
30 | FSA for controller handling
31 |
32 | The main purpose of the 4 different methods defined here is to manage
33 | memory usage. Please follow the format to make sure your game uses memory
34 | effeciently.
35 |
36 | init:
37 | should be the the minimum amout of data needed, such as filenames and
38 | names of other resources to be loaded when context is endered
39 |
40 | enter:
41 | called when context is the running context
42 | load any sounds, images, data files here that are needed
43 |
44 | exit:
45 | unload any data not directly needed, such as images and sounds and all
46 | other data loaded in enter()
47 |
48 | terminate:
49 | unload any other data that you loaded in init()
50 |
51 | """
52 |
53 | def __enter__(self):
54 | self.enter()
55 |
56 | def __exit__(self):
57 | self.exit()
58 |
59 |
60 | def init(self, *args, **kwargs):
61 | """
62 | Called before context is placed in a stack
63 | This will only be called once over the lifetime of a context
64 | """
65 |
66 | pass
67 |
68 |
69 | def enter(self):
70 | """
71 | Called after focus is given to the context
72 | This may be called several times over the lifetime of the context
73 | """
74 |
75 | pass
76 |
77 |
78 | def exit(self):
79 | """
80 | Called after focus is lost
81 | This may be called several times over the lifetime of the context
82 | """
83 |
84 | pass
85 |
86 |
87 | def terminate(self):
88 | """
89 | Called after the context is removed from a stack
90 | This will only be called once
91 | """
92 |
93 | pass
94 |
95 |
96 | class ContextDriver(object):
97 | """
98 | ContextDriver will manage a list of different contexts.
99 |
100 | Contexts are stored in a simple FILO queue.
101 |
102 | The current_context attribute will be the context at the top of the stack.
103 |
104 | When a context is added to the ContextDriver, it will have the 'driver'
105 | attribute set to the Driver. Contexts are welcome to remove themselves or
106 | other contexts.
107 | """
108 |
109 |
110 | def __init__(self):
111 | self._stack = []
112 |
113 |
114 | def remove(self, context, exit=True, terminate=True):
115 | """
116 | remove the context from the stack
117 | """
118 |
119 | old_context = self.current_context
120 | self._stack.remove(context)
121 | if exit:
122 | context.__exit__()
123 |
124 | if context is old_context:
125 | new_context = self.current_context
126 | if new_context:
127 | new_context.__enter__()
128 |
129 | if terminate:
130 | context.terminate()
131 |
132 |
133 | def queue(self, new_context, *args, **kwargs):
134 | """
135 | queue a context just before the current context
136 | when the current context finishes, the context passed will be run
137 | """
138 |
139 | new_context.driver = self
140 | new_context.init(*args, **kwargs)
141 | self._stack.insert(-1, new_context)
142 |
143 |
144 | def append(self, new_context, *args, **kwargs):
145 | """
146 | start a new context and hold the current context.
147 |
148 | idea: the old context could be pickled and stored to disk.
149 | """
150 |
151 | old_context = self.current_context
152 |
153 | new_context.driver = self
154 | new_context.init(*args, **kwargs)
155 | self._stack.append(new_context)
156 |
157 | if old_context:
158 | old_context.__exit__()
159 |
160 | new_context.__enter__()
161 |
162 |
163 | @property
164 | def current_context(self):
165 | try:
166 | return self._stack[-1]
167 | except IndexError:
168 | return None
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/pygoap/environment.py:
--------------------------------------------------------------------------------
1 | """
2 | Since a pyGOAP agent relies on cues from the environment when planning, having
3 | a stable and efficient virtual environment is paramount.
4 |
5 | When coding your game or simulation, you can think of the environment as the
6 | conduit that connects your actors on screen to their simulated thoughts. This
7 | environment simply provides enough basic information to the agents to work. It
8 | is up to you to make it useful.
9 |
10 | objects should be able to produce actions that would be useful, rather than the
11 | virtual actors knowing eacatly how to use and what to do with other objects.
12 | This concept comes from 'The Sims', where each agent doesn't need to know how
13 | to use every object, but can instead query the object for actions to do with it.
14 | """
15 |
16 | from precepts import *
17 | from actionstates import *
18 | from itertools import chain
19 | import logging
20 |
21 | debug = logging.debug
22 |
23 |
24 |
25 | class ObjectBase(object):
26 | """
27 | class for objects that agents can interact with
28 | """
29 |
30 | def __init__(self, name='noname'):
31 | self.name = name
32 | self._condition = {}
33 |
34 | def get_actions(self, other):
35 | """
36 | generate a list of actions that could be used with this object
37 | """
38 | return []
39 |
40 | def condition(self, name):
41 | try:
42 | return self._condition[name]
43 | except KeyError:
44 | return False
45 |
46 | def set_condition(self, name, value):
47 | self._condition[name] = value
48 |
49 | def __repr__(self):
50 | return "".format(self.name)
51 |
52 |
53 | class Environment(object):
54 | """Abstract class representing an Environment. 'Real' Environment classes
55 | inherit from this.
56 | """
57 |
58 | def __init__(self, entities=[], agents=[], time=0):
59 | self.time = time
60 | self._agents = []
61 | self._entities = []
62 | self._positions = {}
63 |
64 | [ self.add(i) for i in entities ]
65 | [ self.add(i) for i in agents ]
66 |
67 | self.action_que = []
68 |
69 | @property
70 | def agents(self):
71 | return iter(self._agents)
72 |
73 | @property
74 | def entities(self):
75 | return chain(self._entities, self._agents)
76 |
77 | def get_position(self, entity):
78 | raise NotImplementedError
79 |
80 |
81 | # this is a placeholder hack. proper handling will go through
82 | # model_precept()
83 | def look(self, caller):
84 | for i in chain(self._entities, self._agents):
85 | caller.process(LocationPrecept(i, self._positions[i]))
86 |
87 |
88 | def run(self, steps=1000):
89 | """
90 | Run the Environment for given number of time steps.
91 | """
92 |
93 | [ self.update(1) for step in xrange(steps) ]
94 |
95 | def add(self, entity, position=None):
96 | """
97 | Add an entity to the environment
98 | """
99 |
100 | from agent import GoapAgent
101 |
102 | debug("[env] adding %s", entity)
103 |
104 |
105 | # hackish way to force agents to re-evaulate their environment
106 | for a in self._agents:
107 | to_remove = []
108 |
109 | for p in a.memory.of_class(DatumPrecept):
110 | if p.name == 'aware':
111 | to_remove.append(p)
112 |
113 | [ a.memory.remove(p) for p in to_remove]
114 |
115 | # add the agent
116 | if isinstance(entity, GoapAgent):
117 | self._agents.append(entity)
118 | entity.environment = self
119 | self._positions[entity] = (None, (0, 0))
120 |
121 | # clever hack to let the planner know who the memory belongs to
122 | entity.process(DatumPrecept('self', entity))
123 | else:
124 | self._entities.append(entity)
125 |
126 | def update(self, time_passed):
127 | """
128 | * Update our time
129 | * Let agents know time has passed
130 | * Update actions that may be running
131 | * Add new actions to the que
132 |
133 | this could be rewritten.
134 | """
135 |
136 | # update time in the simulation
137 | self.time += time_passed
138 |
139 | # let all the agents know that time has passed and bypass the modeler
140 | p = TimePrecept(self.time)
141 | [ a.process(p) for a in self.agents ]
142 |
143 | # get all the running actions for the agents
144 | self.action_que = [ a.running_actions() for a in self.agents ]
145 |
146 | # start any actions that are not started
147 | [ action.__enter__() for action in self.action_que
148 | if action.state == ACTIONSTATE_NOT_STARTED ]
149 |
150 | # update all the actions that may be running
151 | precepts = [ a.update(time_passed) for a in self.action_que ]
152 | precepts = [ p for p in precepts if not p == None ]
153 |
154 |
155 | def broadcast_precepts(self, precepts, agents=None):
156 | """
157 | for efficiency, please use this for sending a list of precepts
158 | """
159 | if agents == None:
160 | agents = self.agents
161 |
162 | model = self.model_precept
163 |
164 | for p in precepts:
165 | [ a.process(model(p, a)) for a in agents ]
166 |
167 | def model_precept(self, precept, other):
168 | """
169 | override this to model the way that precept objects move in the
170 | simulation. by default, all precept objects will be distributed
171 | indiscriminately to all agents.
172 | """
173 | return precept
174 |
175 |
--------------------------------------------------------------------------------
/pygoap/environment2d.py:
--------------------------------------------------------------------------------
1 | """
2 | Since a pyGOAP agent relies on cues from the environment when planning, having
3 | a stable and efficient virtual environment is paramount. This environment is
4 | simply a placeholder and demonstration.
5 |
6 | When coding your game or simulation, you can think of the environment as the
7 | conduit that connects your actors on screen to their simulated thoughts. This
8 | environment simply provides enough basic information to the agents to work. It
9 | is up to you to make it useful.
10 | """
11 |
12 | from agent import GoapAgent
13 | from environment import Environment
14 | from pathfinding.astar import search, Node
15 | from precepts import *
16 | import random, math
17 |
18 |
19 |
20 | def distance((ax, ay), (bx, by)):
21 | "The distance between two (x, y) points."
22 | return math.hypot((ax - bx), (ay - by))
23 |
24 | def distance2((ax, ay), (bx, by)):
25 | "The square of the distance between two (x, y) points."
26 | return (ax - bx)**2 + (ay - by)**2
27 |
28 | def clip(vector, lowest, highest):
29 | """Return vector, except if any element is less than the corresponding
30 | value of lowest or more than the corresponding value of highest, clip to
31 | those values.
32 | >>> clip((-1, 10), (0, 0), (9, 9))
33 | (0, 9)
34 | """
35 | return type(vector)(map(min, map(max, vector, lowest), highest))
36 |
37 |
38 | class Pathfinding2D(object):
39 | def get_surrounding(self, position):
40 | """
41 | Return all positions around this one.
42 | """
43 | x, y = position
44 |
45 | return ((x-1, y-1), (x-1, y), (x-1, y+1), (x, y-1), (x, y+1),
46 | (x+1, y-1), (x+1, y), (x+1, y+1))
47 |
48 | def calc_h(self, position1, position2):
49 | return distance(position1, position2)
50 |
51 | def factory(self, position):
52 |
53 | # EPIC HACK
54 | # fix this when position conventions are standardized
55 | try:
56 | if len(position[1]) == 2:
57 | x, y = position[1]
58 | else:
59 | x, y = position
60 | except TypeError:
61 | x, y = position
62 |
63 | return Node((x, y))
64 |
65 |
66 | class XYEnvironment(Environment, Pathfinding2D):
67 | """
68 | This class is for environments on a 2D plane.
69 |
70 | This class is featured enough to run a simple simulation.
71 | """
72 |
73 | def __init__(self, width=10, height=10):
74 | super(XYEnvironment, self).__init__()
75 | self._positions = {}
76 | self.width = width
77 | self.height = height
78 |
79 | def add(self, entity):
80 | super(XYEnvironment, self).add(entity)
81 | self.set_position(entity, self.default_position())
82 |
83 | def set_position(self, entity, position):
84 | self._positions[entity] = position
85 |
86 | def get_position(self, entity):
87 | return self._positions[entity]
88 |
89 | def model_vision(self, precept, origin, terminus):
90 | return precept
91 |
92 | def model_sound(self, precept, origin, terminus):
93 | return precept
94 |
95 | def look(self, parent, direction=None, distance=None):
96 | """
97 | Simulate vision by sending precepts to the parent.
98 | """
99 |
100 | model = self.model_precept
101 |
102 | for entity in self.entities:
103 | parent.process(
104 | model(
105 | PositionPrecept(
106 | entity, self.get_position(entity)),
107 | parent
108 | )
109 | )
110 |
111 | def objects_at(self, position):
112 | """
113 | Return all objects exactly at a given position.
114 | """
115 |
116 | return [ obj for obj in self.entitys if obj.position == position ]
117 |
118 | def objects_near(self, position, radius):
119 | """
120 | Return all objects within radius of position.
121 | """
122 |
123 | radius2 = radius * radius
124 | return [ obj for obj in self.entitys
125 | if distance2(position, obj.position) <= radius2 ]
126 |
127 | def default_position(self):
128 | loc = (random.randint(0, self.width), random.randint(0, self.height))
129 | return (self, loc)
130 |
131 | def model_precept(self, precept, other):
132 | return precept
133 |
134 | def can_move_from(self, agent, dist=100):
135 | """
136 | return a list of positions that are possible for this agent to be
137 | in if it were to move [dist] spaces or less.
138 | """
139 |
140 | x, y = agent.environment.get_position(agent)[1]
141 | pos = []
142 |
143 | for xx in xrange(x - dist, x + dist):
144 | for yy in xrange(y - dist, y + dist):
145 | if distance2((xx, yy), (x, y)) <= dist:
146 | pos.append((self, (xx, yy)))
147 |
148 | return pos
149 |
150 | def pathfind(self, start, finish):
151 | """
152 | return a path from start to finish
153 | """
154 |
155 | return search(start, finish, self.factory)
156 |
--------------------------------------------------------------------------------
/pygoap/goals.py:
--------------------------------------------------------------------------------
1 | """
2 | Goals in the context of a pyGOAP agent give the planner some direction when
3 | planning. Goals are known to the agent and are constantly monitored and
4 | evaluated. The agent will attempt to choose the most relevant goal for it's
5 | state (determined by the blackboard) and then the planner will determine a
6 | plan for the agent to follow that will (possibly) satisfy the chosen goal.
7 |
8 | See the modules effects.py and goals.py to see how these are used.
9 |
10 | test() should return a float from 0-1 on how successful the action would be
11 | if carried out with the given state of the memory.
12 |
13 | touch() should modify a memory in some meaningful way as if the action was
14 | finished successfully.
15 | """
16 |
17 | from memory import MemoryManager
18 | from environment2d import distance
19 | from precepts import *
20 | import sys, logging
21 |
22 | debug = logging.debug
23 |
24 |
25 |
26 | class GoalBase(object):
27 | """
28 | Goals:
29 | can be tested
30 | can be relevant
31 |
32 | Goals, ActionPrereqs and ActionEffects are now that same class. They share
33 | so much functionality that they have been combined into one class.
34 |
35 | The only difference is how they are used. If a goal is used by the planner
36 | then that will be the final point of the plan. if it is used in
37 | conjunction with an action, then it will function as a prereq.
38 | """
39 |
40 | def __init__(self, *args, **kwargs):
41 | try:
42 | self.condition = args[0]
43 | except IndexError:
44 | self.condition = None
45 |
46 | self.weight = 1.0
47 | self.args = args
48 | self.kw = kwargs
49 |
50 | def touch(self, memory):
51 | pass
52 |
53 | def test(self, memory):
54 | pass
55 |
56 | def get_relevancy(self, memory):
57 | """
58 | will return the "relevancy" value for this goal/prereq.
59 |
60 | as a general rule, the return value here should never equal
61 | what is returned from test()
62 | """
63 | score = 1 - self.test(memory)
64 | return self.weight * score
65 |
66 | def self_test(self):
67 | """
68 | make sure the goal is sane
69 | """
70 | memory = MemoryManager()
71 | self.touch(memory)
72 | assert self.test(memory) == 1.0
73 |
74 | def __repr__(self):
75 | return "<{}>".format(self.__class__.__name__)
76 |
77 |
78 | class SimpleGoal(GoalBase):
79 | def test(self, memory):
80 | total = 0.0
81 | for precept in memory:
82 | for item in self.kw.items():
83 | if precept == item:
84 | total += 1
85 | return total / len(self.kw)
86 |
87 | def touch(self, memory):
88 | for item in self.kw.items():
89 | memory.add(DatumPrecept(*item))
90 |
91 | def __repr__(self):
92 | return "<{}=\"{}\">".format(self.__class__.__name__, self.kw)
93 |
94 |
95 | class EvalGoal(GoalBase):
96 | """
97 | uses what i think is a somewhat safe way of evaluating python statements.
98 |
99 | feel free to contact me if you have a better way
100 | """
101 |
102 | def test(self, memory):
103 | condition = self.args[0]
104 |
105 | # this only works for simple expressions
106 | cmpop = (">", "<", ">=", "<=", "==")
107 |
108 | i = 0
109 | index = 0
110 | expr = condition.split()
111 | while index == 0:
112 | try:
113 | index = expr.index(cmpop[i])
114 | except:
115 | i += 1
116 | if i > 5: break
117 |
118 | try:
119 | side0 = float(eval(" ".join(expr[:index]), memory))
120 | side1 = float(eval(" ".join(expr[index+1:]), memory))
121 | except NameError:
122 | return 0.0
123 |
124 | cmpop = cmpop[i]
125 |
126 | if (cmpop == ">") or (cmpop == ">="):
127 | if side0 == side1:
128 | return 1.0
129 | elif side0 > side1:
130 | v = side0 / side1
131 | elif side0 < side1:
132 | if side0 == 0:
133 | return 0.0
134 | else:
135 | v = 1 - ((side1 - side0) / side1)
136 |
137 | if v > 1: v = 1.0
138 | if v < 0: v = 0.0
139 |
140 | return v
141 |
142 | def touch(self, memory):
143 | def do_it(expr, d):
144 | try:
145 | exec expr in d
146 | except NameError as detail:
147 | name = detail[0].split()[1].strip('\'')
148 | d[name] = 0
149 | do_it(expr, d)
150 |
151 | return d
152 |
153 | d = {}
154 | d['__builtins__'] = None
155 |
156 | for k, v in d.items():
157 | if k == '__builtins__':
158 | continue
159 |
160 | memory.add(DatumPrecept(k, v))
161 |
162 | return True
163 |
164 |
165 | class AlwaysValidGoal(GoalBase):
166 | """
167 | Will always be valid.
168 | """
169 |
170 | def test(self, memory):
171 | return 1.0
172 |
173 |
174 | class NeverValidGoal(GoalBase):
175 | """
176 | Will never be valid.
177 | """
178 |
179 | def test(self, memory):
180 | return 0.0
181 |
182 |
183 | class PositionGoal(GoalBase):
184 | """
185 | This validator is for finding the position of objects.
186 | """
187 |
188 | def test(self, memory):
189 | """
190 | search memory for last known position of the target
191 | if target is not in agent's memory then return 0.0.
192 |
193 | do pathfinding and determine if the target is accessible
194 | if not return 0.0
195 |
196 | Determine the distance required to travel to the target
197 | return 1.0 if the target is reachable
198 | """
199 |
200 | target = self.args[0]
201 | target_position = None
202 |
203 | debug("[PositionGoal] testing %s", self.args)
204 |
205 | # find where the target is, according to the memory
206 | for precept in memory.of_class(PositionPrecept):
207 | if precept.entity is target:
208 | target_position = precept.position
209 | break
210 |
211 | if target_position == self.args[1]:
212 | return 1.0
213 |
214 | else:
215 |
216 | d = distance(position, target_position)
217 | if d > self.dist:
218 | return (float(self.dist / d)) * float(self.dist)
219 | elif d <= self.dist:
220 | return 1.0
221 | else:
222 | return 0.0
223 |
224 | def touch(self, memory):
225 | memory.add(PositionPrecept(self.args[0], self.args[1]))
226 |
227 |
228 | class HasItemGoal(GoalBase):
229 | """
230 | returns true if item is in inventory (according to memory)
231 |
232 | when creating instance, 'owner' must be passed as a keyword.
233 | its value can be any game object that is capable of holding an object
234 |
235 | NOTE: testing can be true to many different objects,
236 | but touching requires a specific object to function
237 |
238 | any other keyword will be evaluated against precepts in the memory passed.
239 | """
240 |
241 | def test(self, memory):
242 | for precept in memory.of_class(PositionPrecept):
243 | if (precept.position[0] == 'self' and
244 | precept.entity == self.args[0]):
245 | return 1.0
246 |
247 | return 0.0
248 |
249 | def touch(self, memory):
250 | memory.add(PositionPrecept(self.args[0], ('self', 0)))
251 |
--------------------------------------------------------------------------------
/pygoap/memory.py:
--------------------------------------------------------------------------------
1 | """
2 | Memories are stored precepts.
3 | """
4 |
5 |
6 |
7 | class MemoryManager(set):
8 | """
9 | Store and manage precepts.
10 |
11 | shared blackboards violate reality in that multiple agents share the same
12 | thoughts, to extend the metaphore. but, the advantage of this is that in
13 | a real-time simulation, it gives the player the impression that the agents
14 | are able to collobroate in some meaningful way, without a significant
15 | impact in performace.
16 |
17 | that being said, i have chosen to restrict blackboards to a per-agent
18 | basis. this library is meant for rpgs, where the action isn't real-time
19 | and would require a more realistic simulation of intelligence.
20 | """
21 |
22 | def add(self, other):
23 | if len(self) > 20:
24 | self.pop()
25 | super(MemoryManager, self).add(other)
26 |
27 | def of_class(self, klass):
28 | for i in self:
29 | if isinstance(i, klass):
30 | yield i
31 |
32 |
--------------------------------------------------------------------------------
/pygoap/planning.py:
--------------------------------------------------------------------------------
1 | from memory import MemoryManager
2 | from actionstates import *
3 | from actions import *
4 |
5 | from heapq import heappop, heappush, heappushpop
6 | from itertools import permutations, chain
7 | import logging
8 | import sys
9 |
10 | debug = logging.debug
11 |
12 |
13 |
14 | def get_children(parent0, parent, builders):
15 | def get_used_class(node):
16 | while node.parent is not None:
17 | yield node.builder
18 | node = node.parent
19 |
20 | used_class = set(get_used_class(parent))
21 |
22 | for builder in builders:
23 | if builder in used_class:
24 | continue
25 |
26 | for action in builder(parent0, parent.memory):
27 | node = PlanningNode(parent, builder, action)
28 | yield node
29 |
30 |
31 | def calcG(node):
32 | cost = node.cost
33 | while not node.parent == None:
34 | node = node.parent
35 | cost += node.cost
36 | return cost
37 |
38 |
39 | class PlanningNode(object):
40 | """
41 | """
42 |
43 | def __init__(self, parent, builder, action, memory=None):
44 | self.parent = parent
45 | self.builder = builder
46 | self.action = action
47 | self.memory = MemoryManager()
48 | self.delta = MemoryManager()
49 | #self.cost = action.calc_cost()
50 | self.cost = 1
51 | self.g = calcG(self)
52 | self.h = 1
53 |
54 | if parent:
55 | self.memory.update(parent.memory)
56 |
57 | elif memory:
58 | self.memory.update(memory)
59 |
60 | action.touch(self.memory)
61 | action.touch(self.delta)
62 |
63 | def __eq__(self, other):
64 | if isinstance(other, PlanningNode):
65 | return self.delta == other.delta
66 | else:
67 | return False
68 |
69 | def __repr__(self):
70 | if self.parent:
71 | return "" % \
72 | (self.action.__class__.__name__,
73 | self.cost,
74 | self.parent.action.__class__.__name__)
75 |
76 | else:
77 | return "" % \
78 | (self.action.__class__.__name__,
79 | self.cost)
80 |
81 |
82 | def plan(parent, builders, start_action, start_memory, goal):
83 | """
84 | Return a list of builders that could be called to satisfy the goal.
85 | Cannot duplicate builders in the plan
86 | """
87 |
88 | # the pushback is used to limit node access in the heap
89 | pushback = None
90 | keyNode = PlanningNode(None, None, start_action, start_memory)
91 | openlist = [(0, keyNode)]
92 | closedlist = []
93 |
94 | debug("[plan] solve %s starting from %s", goal, start_action)
95 | debug("[plan] memory supplied is %s", start_memory)
96 |
97 | success = False
98 | while openlist or pushback:
99 |
100 | # get the best node and remove it from the openlist
101 | if pushback is None:
102 | keyNode = heappop(openlist)[1]
103 | else:
104 | keyNode = heappushpop(openlist,
105 | (pushback.g + pushback.h, pushback))[1]
106 | pushback = None
107 |
108 | # if our goal is satisfied, then stop
109 | if goal.test(keyNode.memory):
110 | success = True
111 | debug("[plan] successful %s", keyNode.action)
112 | break
113 |
114 | for child in get_children(parent, keyNode, builders):
115 | if child in openlist:
116 | possG = keyNode.g + child.cost
117 | if (possG < child.g):
118 | child.parent = keyNode
119 | child.g = calcG(child)
120 | # TODO: update the h score
121 | else:
122 | # add node to our openlist, using pushpack if needed
123 | if pushback is None:
124 | heappush(openlist, (child.g + child.h, child))
125 | else:
126 | heappush(openlist, (pushback.g + pushback.h, pushback))
127 | pushback = child
128 |
129 | if success:
130 | path = [keyNode.action]
131 | while keyNode.parent is not None:
132 | keyNode = keyNode.parent
133 | path.append(keyNode.action)
134 |
135 | return path
136 |
137 | else:
138 | return []
139 |
140 |
141 |
--------------------------------------------------------------------------------
/pygoap/precepts.py:
--------------------------------------------------------------------------------
1 | """
2 | Contains a set of useful precept types for use with an environment.
3 | """
4 |
5 | from collections import namedtuple as nt
6 |
7 |
8 | # used to remember where entities are
9 | PositionPrecept = nt('PositionPrecept', 'entity, position')
10 |
11 | # used to remember what the time is
12 | TimePrecept = nt('TimePrecept', 'time')
13 |
14 | # used to remember a single piece of data
15 | DatumPrecept = nt('DatumPrecept', 'name, value')
16 |
--------------------------------------------------------------------------------
/pygoap/tiledenvironment.py:
--------------------------------------------------------------------------------
1 | from environment2d import XYEnvironment
2 | import tmxloader
3 | from pygame import Surface
4 |
5 |
6 |
7 | class TiledEnvironment(XYEnvironment):
8 | """
9 | Environment that can use Tiled Maps
10 | """
11 |
12 | def __init__(self, filename):
13 | self.filename = filename
14 | self.tiledmap = tmxloader.load_pygame(self.filename)
15 |
16 | super(TiledEnvironment, self).__init__()
17 |
18 | def render(self, surface):
19 | # not going for effeciency here
20 |
21 | for l in xrange(0, len(self.tiledmap.layers)):
22 | for y in xrange(0, self.tiledmap.height):
23 | for x in xrange(0, self.tiledmap.width):
24 | tile = self.tiledmap.getTileImage(x, y, l)
25 | xx = x * self.tiledmap.tilewidth
26 | yy = y * self.tiledmap.tileheight
27 | if not tile == 0:
28 | surface.blit(tile, (xx, yy))
29 |
30 | for t in self.entities:
31 | env, (x, y) = self.get_position(t)
32 | x *= self.tiledmap.tilewidth
33 | y *= self.tiledmap.tileheight
34 |
35 | s = Surface((self.tiledmap.tilewidth, self.tiledmap.tileheight))
36 | s.fill((128,0,0))
37 |
38 | surface.blit(s, (x, y))
39 |
40 | def __repr__(self):
41 | return "T-Env"
42 |
43 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | """
2 | lets make a drunk pirate.
3 |
4 | scenerio:
5 | the pirate begins by idling
6 | soon....he spies rum...
7 |
8 | he should attempt to get drunk...
9 | ...any way he knows how.
10 | """
11 |
12 | __version__ = ".013"
13 |
14 | from pygoap.agent import GoapAgent
15 | from pygoap.environment import ObjectBase
16 | from pygoap.tiledenvironment import TiledEnvironment
17 | from pygoap.goals import *
18 | import os, imp, sys
19 |
20 | from pygame.locals import *
21 |
22 | import logging
23 | logging.basicConfig(level=00)
24 |
25 |
26 |
27 | stdout = sys.stdout
28 | global_actions = {}
29 |
30 |
31 | # somewhat hackish way to load actions (they are in the npcs/pirate folder)
32 | def load_commands(agent, path):
33 | mod = imp.load_source("actions", os.path.join(path, "actions.py"))
34 | global_actions = dict([ (c.__name__, c()) for c in mod.exported_actions ])
35 |
36 | for k, v in global_actions.items():
37 | print "testing action {}...".format(v)
38 |
39 | [ agent.add_action(a) for a in global_actions.values() ]
40 |
41 |
42 | # precept filter for the pirate to search for females
43 | def is_female(precept):
44 | try:
45 | thing = precept.thing
46 | except AttributeError:
47 | return False
48 | else:
49 | if isinstance(thing, Human):
50 | return thing.gender == "Female"
51 |
52 | # subclass the basic GoapAgent class to give them gender and names
53 | class Human(GoapAgent):
54 | def __init__(self, gender, name="welp"):
55 | super(Human, self).__init__()
56 | self.gender = gender
57 | self.name = name
58 |
59 | def __repr__(self):
60 | return "" % self.gender
61 |
62 |
63 | def run_once():
64 | import pygame
65 |
66 | pygame.init()
67 | screen = pygame.display.set_mode((480, 480))
68 | pygame.display.set_caption('Pirate Island')
69 |
70 | screen_buf = pygame.Surface((240, 240))
71 |
72 | # make our little cove is a Tiled TMX map
73 | formosa = TiledEnvironment("formosa.tmx")
74 |
75 | time = 0
76 | interactive = 1
77 |
78 | run = True
79 | while run:
80 | stdout.write("=============== STEP {} ===============\n".format(time))
81 |
82 | formosa.run(1)
83 |
84 |
85 | # for the demo, we add items at different timesteps to debug and monitor
86 | # changes in the planner
87 |
88 | # add the pirate
89 | if time == 1:
90 | pirate = Human("Male", "jack")
91 | load_commands(pirate, os.path.join("npc", "pirate"))
92 |
93 | # give the pirate a goal that makes him want to get drunk
94 | pirate.add_goal(SimpleGoal(is_drunk=True))
95 |
96 | # this goal forces the agent to scan the environment if something
97 | # changes. think of it as 'desire to be aware of the surroundings'
98 | pirate.add_goal(SimpleGoal(aware=True))
99 |
100 | formosa.add(pirate)
101 |
102 | # positions are a tuple: (container, (x, y))
103 | # this allows for objects to hold other objects
104 | formosa.set_position(pirate, (formosa, (0,0)))
105 |
106 | elif time == 3:
107 |
108 | # create rum and add it
109 | rum = ObjectBase("rum")
110 | formosa.add(rum)
111 | formosa.set_position(rum, (formosa, (2,5)))
112 |
113 | elif time == 6:
114 |
115 | # create a woman and add her
116 | wench = Human("Female", "wench")
117 | formosa.add(wench)
118 |
119 | if time >= 1:
120 | if pirate.condition('drunk'):
121 | print "YAY! A drunk pirate is a happy pirate!"
122 | print "Test concluded"
123 |
124 | # clear the screen and paint the map
125 | screen_buf.fill((0,128,255))
126 | formosa.render(screen_buf)
127 | pygame.transform.scale2x(screen_buf, screen)
128 | pygame.display.flip()
129 |
130 | stdout.write("\nPRESS ANY KEY TO CONTINUE".format(time))
131 | stdout.flush()
132 |
133 | # wait for a keypress
134 | try:
135 | if interactive:
136 | event = pygame.event.wait()
137 | else:
138 | event = pygame.event.poll()
139 | while event:
140 | if event.type == QUIT:
141 | run = False
142 | break
143 |
144 | if event.type == KEYDOWN:
145 | if event.key == K_ESCAPE:
146 | run = False
147 | break
148 |
149 | if not interactive: break
150 |
151 | if event.type == KEYUP: break
152 |
153 | if interactive:
154 | event = pygame.event.wait()
155 | else:
156 | event = pygame.event.poll()
157 |
158 | except KeyboardInterrupt:
159 | run = False
160 |
161 | stdout.write("\n\n");
162 | time += 1
163 |
164 | if time == 32: run = False
165 |
166 |
167 | if __name__ == "__main__":
168 | import cProfile
169 | import pstats
170 |
171 | try:
172 | cProfile.run('run_once()', "pirate.prof")
173 | except KeyboardInterrupt:
174 | pass
175 |
176 | p = pstats.Stats("pirate.prof")
177 | p.strip_dirs()
178 | p.sort_stats('time').print_stats(20, "^((?!pygame).)*$")
179 | p.sort_stats('time').print_stats(20)
180 |
--------------------------------------------------------------------------------
/tmxloader.py:
--------------------------------------------------------------------------------
1 | """
2 | Map loader for TMX Files
3 | bitcraft (leif dot theden at gmail.com)
4 | v.13 - for python 2.7
5 |
6 | If you have any problems or suggestions, please contact me via email.
7 | Tested with Tiled 0.8.0 for Mac.
8 |
9 | released under the GPL v3
10 |
11 | ===============================================================================
12 |
13 | This map loader can be used to load maps created in the Tiled map editor. It
14 | provides a simple way to get tiles and associated metadata so that you can draw
15 | a map onto the screen.
16 |
17 | This is *not* a rendering engine. It will load the data that is necessary to
18 | render a map onto the screen. All tiles will be loaded into in memory and
19 | available to blit onto the screen.
20 |
21 |
22 | Design Goals:
23 | Simple api
24 | Memory efficient and fast
25 |
26 | Features:
27 | Loads data and "properties" metadata from Tile's TMX format
28 | "Properties" for: maps, tilesets, layers, objectgroups, objects, and tiles
29 | Automatic flipping and rotation of tiles
30 | Supports base64, csv, gzip, zlib and uncompressed TMX
31 | Image loading with pygame
32 |
33 | Missing:
34 | Polyline (new in 0.8.0)
35 | Polygon (new in 0.8.0)
36 |
37 |
38 | New in .13:
39 | loader: Removed duplicates returned from getTilePropertiesByLayer
40 | loader: Modified confusing messages for GID errors
41 | loader: Fixed bug where transformed tile properties are not available
42 | loader: No longer loads metadata for tiles that are not used
43 | loader: Reduced tile cache to 256 unique tiles
44 | loader: Removed 'visible' from list of reserved words
45 | loader: Added 'buildDistributionRects' and maputils module
46 | loader: Added some misc. functions for retrieving properties
47 | pygame: Smarter tile management made tile loading cache useless; removed it
48 | pygame: pygame.RLEACCEL flag added when appropriate
49 |
50 | New in .12:
51 | loader: Fixed bug where tile properties could contain reserved words
52 | loader: Reduced size of image index by only allocating space for used tiles
53 |
54 | New in .11:
55 | loader: Added support for tileset properties
56 | loader: Now checks for property names that are reserved for internal use
57 | loader: Added support for rotated tiles
58 | pygame: Only the tiles that are used in the map will be loaded into memory
59 | pygame: Added support for rotated tiles
60 | pygame: Added option to force a bitsize (depth) for surfaces
61 | pygame: Added option to convert alpha transparency to colorkey transparency
62 | pygame: Tilesets no longer load with per-pixel alphas by default
63 | pygame: Colorkey transparency should be correctly handled now
64 |
65 |
66 | NOTES:
67 |
68 | * The Tiled "properties" have reserved names.
69 |
70 | If you use "properties" for any of the following object types, you cannot use
71 | any of theese words as a name for your property. A ValueError will be raised
72 | if there are any conflicts.
73 |
74 | As of 0.8.0, these values are:
75 |
76 | map: version, orientation, width, height, tilewidth, tileheight
77 | properties, tileset, layer, objectgroup
78 |
79 | tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin,
80 | image, tile, properties
81 |
82 | tile: id, image, properties
83 |
84 | layer: name, x, y, width, height, opacity, properties, data
85 |
86 | objectgroup: name, color, x, y, width, height, opacity, object, properties
87 |
88 | object: name, type, x, y, width, height, gid, properties, polygon,
89 | polyline, image
90 |
91 |
92 |
93 | I have been intentionally not including a rendering utility since rendering a
94 | map will not be the same in every situation. However, I can appreciate that
95 | some poeple won't understand how it works unless they see it, so I am including
96 | a sample map and viewer.
97 |
98 | I've included a copy of this loader that may work with python 3.x. I
99 | personally do not think that python 3.x should be used with pygame, yet (and I
100 | am not the only person). You can try it if you insist on using pygame with
101 | python 3.x, but I don't update that often.
102 |
103 | ===============================================================================
104 |
105 | Basic usage sample:
106 |
107 | >>> import tmxloader
108 | >>> tmxdata = tmxloader.load_pygame("map.tmx")
109 |
110 |
111 | When you want to draw tiles, you simply call "get_tile_image":
112 |
113 | >>> image = tmxdata.get_tile_image(x, y, layer)
114 | >>> screen.blit(position, image)
115 |
116 |
117 | Maps, tilesets, layers, objectgroups, and objects all have a simple way to
118 | access metadata that was set inside tiled: they all become object attributes.
119 |
120 | >>> layer = tmxdata.layers[0]
121 | >>> print layer.tilewidth
122 | 32
123 | >>> print layer.weather
124 | 'sunny'
125 |
126 |
127 | Tiles properties are the exception here, and must be accessed through
128 | "getTileProperties". The data is a regular Python dictionary:
129 |
130 | >>> tile = tmxdata.getTileProperties(x, y, layer)
131 | >>> tile["name"]
132 | 'CobbleStone'
133 |
134 | """
135 |
136 | from itertools import chain, product
137 |
138 |
139 | # internal flags
140 | TRANS_FLIPX = 1
141 | TRANS_FLIPY = 2
142 | TRANS_ROT = 4
143 |
144 |
145 | # Tiled gid flags
146 | GID_TRANS_FLIPX = 1<<31
147 | GID_TRANS_FLIPY = 1<<30
148 | GID_TRANS_ROT = 1<<29
149 |
150 |
151 | class TiledElement(object):
152 | pass
153 |
154 | class TiledMap(TiledElement):
155 | """
156 | not really useful unless "loaded" ie: don't instance directly.
157 | see the pygame loader for inspiration
158 |
159 | In the interest of memory consumption, this loader ignores any tiles that
160 | are never actually displayed on the map. As a consequence, the GID's that
161 | are stored in Tiled and the TMX format will not be the same in most cases.
162 | """
163 |
164 | reserved = "version orientation width height tilewidth tileheight properties tileset layer objectgroup".split()
165 |
166 |
167 | def __init__(self):
168 | from collections import defaultdict
169 |
170 | TiledElement.__init__(self)
171 | self.layers = [] # list of all layers
172 | self.tilesets = [] # list of TiledTileset objects
173 | self.tilelayers = [] # list of TiledLayer objects
174 | self.objectgroups = [] # list of TiledObjectGroup objects
175 | self.tile_properties = {} # dict of tiles that have metadata
176 | self.gidmap = {} # mapping between gid that are loaded
177 | self.filename = None
178 |
179 | self.visibleTileLayers = [] # list of tile layers that should be drawn
180 |
181 | # should be filled in by a loader function
182 | self.images = []
183 |
184 | # defaults from the TMX specification
185 | self.version = 0.0
186 | self.orientation = None
187 | self.width = 0
188 | self.height = 0
189 | self.tilewidth = 0
190 | self.tileheight = 0
191 |
192 | self.transgids = defaultdict(list) # keep record of tiles to modify
193 | self.imagemap = {} # mapping of gid and trans flags to real gids
194 | self.loadgids = [] # gids that should be loaded for display
195 | self.maxgid = 1
196 |
197 |
198 | def getTileImage(self, x, y, layer):
199 | """
200 | return the tile image for this location
201 | x and y must be integers and are in tile coordinates, not pixel
202 |
203 | return value will be 0 if there is no tile with that location.
204 | """
205 |
206 | try:
207 | gid = self.tilelayers[int(layer)].data[int(y)][int(x)]
208 | except (IndexError, ValueError):
209 | msg = "Coords: ({0},{1}) in layer {2} is invalid."
210 | raise Exception, msg.format(x, y, layer)
211 | except TypeError:
212 | msg = "Tiles must be specified in integers."
213 | raise TypeError, msg
214 |
215 | else:
216 | try:
217 | return self.images[gid]
218 | except (IndexError, ValueError):
219 | msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}"
220 | raise Exception, msg.format(x, y, layer, gid)
221 |
222 |
223 | def getTileGID(self, x, y, layer):
224 | """
225 | return GID of a tile in this location
226 | x and y must be integers and are in tile coordinates, not pixel
227 | """
228 |
229 | try:
230 | return self.tilelayers[int(layer)].data[int(y)][int(x)]
231 | except (IndexError, ValueError):
232 | msg = "Coords: ({0},{1}) in layer {2} is invalid"
233 | raise Exception, msg.format(x, y, layer)
234 |
235 |
236 | def getDrawOrder(self):
237 | """
238 | return a list of objects in the order that they should be drawn
239 | this will also exclude any layers that are not set to visible
240 |
241 | may be useful if you have objects and want to control rendering
242 | from tiled
243 | """
244 |
245 | raise NotImplementedError
246 |
247 |
248 | def getTileImages(self, r, layer):
249 | """
250 | return a group of tiles in an area
251 | expects a pygame rect or rect-like list/tuple
252 |
253 | usefull if you don't want to repeatedly call get_tile_image
254 | """
255 |
256 | raise NotImplementedError
257 |
258 |
259 | def getObjects(self):
260 | """
261 | Return iterator of all the objects associated with this map
262 | """
263 |
264 | return chain(*[ i.objects for i in self.objectgroups ])
265 |
266 |
267 | def getTileProperties(self, (x, y, layer)):
268 | """
269 | return the properties for the tile, if any
270 | x and y must be integers and are in tile coordinates, not pixel
271 |
272 | returns a dict of there are properties, otherwise will be None
273 | """
274 |
275 | try:
276 | gid = self.tilelayers[int(layer)].data[int(y)][int(x)]
277 | except (IndexError, ValueError):
278 | msg = "Coords: ({0},{1}) in layer {2} is invalid."
279 | raise Exception, msg.format(x, y, layer)
280 |
281 | else:
282 | try:
283 | return self.tile_properties[gid]
284 | except (IndexError, ValueError):
285 | msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}"
286 | raise Exception, msg.format(x, y, layer, gid)
287 | except KeyError:
288 | return None
289 |
290 |
291 | def getLayerData(self, layer):
292 | """
293 | Return the data for a layer.
294 |
295 | Data is an array of arrays.
296 |
297 | pos = data[y][x]
298 | """
299 |
300 | try:
301 | return self.tilelayers[layer].data
302 | except IndexError:
303 | msg = "Layer {} does not exist."
304 | raise ValueError, msg.format(layer)
305 |
306 |
307 | def getTileLocation(self, gid):
308 | # experimental way to find locations of a tile by the GID
309 |
310 | p = product(xrange(self.width),
311 | xrange(self.height),
312 | xrange(len(self.tilelayers)))
313 |
314 | return [ (x,y,l) for (x,y,l) in p
315 | if self.tilelayers[l].data[y][x] == gid ]
316 |
317 |
318 | def getTilePropertiesByGID(self, gid):
319 | try:
320 | return self.tile_properties[gid]
321 | except KeyError:
322 | return None
323 |
324 |
325 | def getTilePropertiesByLayer(self, layer):
326 | """
327 | Return a list of tile properties (dict) in use in this tile layer.
328 | """
329 |
330 | try:
331 | layer = int(layer)
332 | except:
333 | msg = "Layer must be an integer"
334 | raise ValueError, msg
335 |
336 | p = product(range(self.width),range(self.height))
337 | layergids = set( self.tilelayers[layer].data[y][x] for x, y in p )
338 |
339 | props = []
340 | for gid in layergids:
341 | try:
342 | props.append((gid, self.tile_properties[gid]))
343 | except:
344 | continue
345 |
346 | return props
347 |
348 |
349 | def loadTileImages(self, filename):
350 | raise NotImplementedError
351 |
352 |
353 | # the following classes get their attributes filled in with the loader
354 |
355 | class TiledTileset(TiledElement):
356 | reserved = "firstgid source name tilewidth tileheight spacing margin image tile properties".split()
357 |
358 | def __init__(self):
359 | TiledElement.__init__(self)
360 | self.lastgid = 0
361 |
362 | # defaults from the specification
363 | self.firstgid = 0
364 | self.source = None
365 | self.name = None
366 | self.tilewidth = 0
367 | self.tileheight = 0
368 | self.spacing = 0
369 | self.margin = 0
370 |
371 | class TiledLayer(TiledElement):
372 | reserved = "name x y width height opacity properties data".split()
373 |
374 | def __init__(self):
375 | TiledElement.__init__(self)
376 | self.data = None
377 |
378 | # defaults from the specification
379 | self.name = None
380 | self.opacity = 1.0
381 | self.visible = True
382 |
383 | class TiledObjectGroup(TiledElement):
384 | reserved = "name color x y width height opacity object properties".split()
385 |
386 | def __init__(self):
387 | TiledElement.__init__(self)
388 | self.objects = []
389 |
390 | # defaults from the specification
391 | self.name = None
392 |
393 | class TiledObject(TiledElement):
394 | __slots__ = "reserved name type x y width height gid".split()
395 | reserved = "name type x y width height gid properties polygon polyline image".split()
396 |
397 | def __init__(self):
398 | TiledElement.__init__(self)
399 |
400 | # defaults from the specification
401 | self.name = None
402 | self.type = None
403 | self.x = 0
404 | self.y = 0
405 | self.width = 0
406 | self.height = 0
407 | self.gid = 0
408 |
409 |
410 | def load_tmx(filename, *args, **kwargs):
411 | """
412 | Utility function to parse a Tiled TMX and return a usable object.
413 | Images will not be loaded, so probably not useful to call this directly
414 | (unless you just want the data).
415 |
416 | See the load_pygame func for an idea of what to do if you want to extend
417 | this further to load images.
418 | """
419 |
420 | from xml.dom.minidom import parse
421 | from itertools import tee, islice, izip, chain, imap
422 | from collections import defaultdict
423 | from struct import unpack
424 | import array, os
425 |
426 |
427 | def handle_bool(text):
428 | # properly convert strings to a bool
429 | try:
430 | return bool(int(text))
431 | except:
432 | pass
433 |
434 | try:
435 | text = str(text).lower()
436 | if text == "true": return True
437 | if text == "yes": return True
438 | if text == "no": return False
439 | if text == "false": return False
440 | except:
441 | pass
442 |
443 | raise ValueError
444 |
445 | # used to change the unicode string returned from minidom to
446 | # proper python variable types.
447 | types = defaultdict(lambda: str)
448 | types.update({
449 | "version": float,
450 | "orientation": str,
451 | "width": int,
452 | "height": int,
453 | "tilewidth": int,
454 | "tileheight": int,
455 | "firstgid": int,
456 | "source": str,
457 | "name": str,
458 | "spacing": int,
459 | "margin": int,
460 | "source": str,
461 | "trans": str,
462 | "id": int,
463 | "opacity": float,
464 | "visible": handle_bool,
465 | "encoding": str,
466 | "compression": str,
467 | "gid": int,
468 | "type": str,
469 | "x": int,
470 | "y": int,
471 | "value": str,
472 | })
473 |
474 | def pairwise(iterable):
475 | # return a list as a sequence of pairs
476 | a, b = tee(iterable)
477 | next(b, None)
478 | return izip(a, b)
479 |
480 | def group(l, n):
481 | # return a list as a sequence of n tuples
482 | return izip(*[islice(l, i, None, n) for i in xrange(n)])
483 |
484 | def parse_properties(node):
485 | """
486 | parse a node and return a dict that represents a tiled "property"
487 | """
488 |
489 | d = {}
490 |
491 | for child in node.childNodes:
492 | if child.nodeName == "properties":
493 | for subnode in child.getElementsByTagName("property"):
494 | # the "properties" from tiled's tmx have an annoying
495 | # quality that "name" and "value" is included.
496 | # here we mangle it to get that stuff out.
497 | d.update(dict(pairwise(
498 | [str(i.value) for i in subnode.attributes.values()])))
499 |
500 | return d
501 |
502 | def get_properties(node, reserved=[]):
503 | """
504 | parses a node and returns a dict that contains the data from the node's
505 | attributes and any data from "property" elements as well. Names will
506 | be checked to make sure that they do not conflict with reserved names.
507 | """
508 |
509 | d = {}
510 |
511 | # set the attributes that are set by tiled
512 | d.update(get_attributes(node))
513 |
514 | # set the attributes that are derived from tiled 'properties'
515 | for k,v in parse_properties(node).items():
516 | if k in reserved:
517 | msg = "The name \"{}\" is reserved cannot be used.\nPlease change the name in Tiled and try again."
518 | raise ValueError, msg.format(k)
519 |
520 | d[k] = v
521 |
522 | return d
523 |
524 | def set_properties(obj, node):
525 | """
526 | read the xml attributes and tiled "properties" from a xml node and fill
527 | in the values into the object's dictionary. Names will be checked to
528 | make sure that they do not conflict with reserved names.
529 | """
530 |
531 | # set the attributes from reserved for tiled
532 | [ setattr(obj, k, types[str(k)](v))
533 | for k,v in get_attributes(node).items() ]
534 |
535 | # set the attributes that are derived from tiled 'properties'
536 | for k,v in parse_properties(node).items():
537 | if k in obj.reserved:
538 | msg = "{} has a property called \"{}\".\nThis name is reserved for {} objects and can cannot be used.\nPlease change the name in Tiled and try again."
539 | raise ValueError, msg.format(obj.name, k, obj.__class__.__name__)
540 | setattr(obj, k, types[str(k)](v))
541 |
542 |
543 | def get_attributes(node):
544 | """
545 | get the attributes from a node and fix them to the correct type
546 | """
547 |
548 | return dict([ (str(k), types[str(k)](v))
549 | for (k,v) in node.attributes.items() ])
550 |
551 |
552 | def decode_gid(raw_gid):
553 | # gid's are encoded with extra information
554 | # as of 0.7.0 it determines if the tile should be flipped when rendered
555 | # as of 0.8.0 bit 30 determines if tip is rotated
556 |
557 | flags = 0
558 | if raw_gid & GID_TRANS_FLIPX == GID_TRANS_FLIPX: flags += TRANS_FLIPX
559 | if raw_gid & GID_TRANS_FLIPY == GID_TRANS_FLIPY: flags += TRANS_FLIPY
560 | if raw_gid & GID_TRANS_ROT == GID_TRANS_ROT: flags += TRANS_ROT
561 | gid = raw_gid & ~(GID_TRANS_FLIPX | GID_TRANS_FLIPY | GID_TRANS_ROT)
562 |
563 | return gid, flags
564 |
565 |
566 | def parse_tileset(node, firstgid=None, mapping=None):
567 | """
568 | parse a tileset element and return a tileset object and properties for
569 | tiles as a dict
570 |
571 | if mapping is specified, the gid of tiles found will be used as a key,
572 | and will be changed to the value of the key. gids not found in the
573 | dict will not be loaded
574 | """
575 |
576 | tileset = TiledTileset()
577 | set_properties(tileset, node)
578 | tiles = {}
579 |
580 | if firstgid != None:
581 | tileset.firstgid = firstgid
582 |
583 | # since tile objects [probably] don't have a lot of metadata,
584 | # we store it seperately from the class itself
585 | for child in node.childNodes:
586 | if child.nodeName == "tile":
587 | p = get_properties(child)
588 | gid = p["id"] + tileset.firstgid
589 | if mapping == None:
590 | del p["id"]
591 | tiles[gid] = p
592 | elif isinstance(mapping, dict):
593 | try:
594 | tiles[mapping[gid]] = p
595 | del p["id"]
596 | except KeyError:
597 | pass
598 | else:
599 | msg = "mapping supplied to parse_tileset must be a dict"
600 | raise TypeError, msg
601 |
602 | # check for tiled "external tilesets"
603 | if tileset.source:
604 | if tileset.source[-4:].lower() == ".tsx":
605 | # we need to mangle the path - tiled stores relative paths
606 | dirname = os.path.dirname(filename)
607 | path = os.path.abspath(os.path.join(dirname, tileset.source))
608 | try:
609 | tsx = parse(path)
610 | except IOError:
611 | msg = "Cannot load external tileset: {}"
612 | raise Exception, msg.format(path)
613 |
614 | tileset_node = tsx.getElementsByTagName("tileset")[0]
615 | tileset, tiles = parse_tileset(tileset_node, \
616 | tileset.firstgid, mapping)
617 | else:
618 | msg = "Found external tileset, but cannot handle type: {}"
619 | raise Exception, msg.format(tileset.source)
620 |
621 | # if we have an "image" tag, process it here
622 | try:
623 | image_node = node.getElementsByTagName("image")[0]
624 | except IndexError:
625 | pass
626 | else:
627 | attr = get_attributes(image_node)
628 | tileset.source = attr["source"]
629 | tileset.trans = attr.get("trans", None)
630 |
631 | # calculate the number of tiles in this tileset
632 | x, r = divmod(attr["width"], tileset.tilewidth)
633 | y, r = divmod(attr["height"], tileset.tileheight)
634 |
635 | tileset.lastgid = tileset.firstgid + x + y
636 |
637 | return tileset, tiles
638 |
639 |
640 | def parse_layer(tmxdata, node):
641 | """
642 | parse a layer element
643 | returns a layer object and the gids that are used in it
644 | """
645 |
646 | layer = TiledLayer()
647 | layer.data = []
648 | set_properties(layer, node)
649 |
650 | data = None
651 | next_gid = None
652 |
653 | data_node = node.getElementsByTagName("data")[0]
654 | attr = get_attributes(data_node)
655 |
656 | if attr["encoding"] == "base64":
657 | from base64 import decodestring
658 | data = decodestring(data_node.lastChild.nodeValue)
659 |
660 | elif attr["encoding"] == "csv":
661 | next_gid = imap(int, "".join(
662 | [ line.strip() for line in data_node.lastChild.nodeValue ]
663 | ).split(","))
664 |
665 | elif not attr["encoding"] == None:
666 | msg = "TMX encoding type: {} is not supported."
667 | raise Exception, msg.format(str(attr["encoding"]))
668 |
669 | if attr["compression"] == "gzip":
670 | from StringIO import StringIO
671 | import gzip
672 | with gzip.GzipFile(fileobj=StringIO(data)) as fh:
673 | data = fh.read()
674 |
675 | elif attr["compression"] == "zlib":
676 | try:
677 | import zlib
678 | except:
679 | msg = "Cannot import zlib. Make sure it is installed."
680 | raise Exception, msg
681 |
682 | data = zlib.decompress(data)
683 |
684 | elif not attr["compression"] == None:
685 | msg = "TMX compression type: {} is not supported."
686 | raise Exception, msg.format(str(attr["compression"]))
687 |
688 | # if data is None, then it was not decoded or decompressed, so
689 | # we assume here that it is going to be a bunch of tile elements
690 | # TODO: this will probably raise an exception if there are no tiles
691 | if attr["encoding"] == next_gid == None:
692 | def get_children(parent):
693 | for child in parent.getElementsByTagName("tile"):
694 | yield int(child.getAttribute("gid"))
695 |
696 | next_gid = get_children(data_node)
697 |
698 | elif not data == None:
699 | # data is a list of gid's. cast as 32-bit ints to format properly
700 | next_gid=imap(lambda i:unpack("