Skip to content

Commit 0c1eb1c

Browse files
authored
Merge pull request #1979 from FoamyGuy/neko_cat
adding circuitpython neko cat
2 parents 99a7e2d + 8779437 commit 0c1eb1c

File tree

3 files changed

+392
-0
lines changed

3 files changed

+392
-0
lines changed

CircuitPython_Neko_Cat/code.py

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
import time
2+
import random
3+
import board
4+
import displayio
5+
import adafruit_imageload
6+
7+
# display background color
8+
BACKGROUND_COLOR = 0x00AEF0
9+
10+
# how long to wait between animation frames in seconds
11+
ANIMATION_TIME = 0.3
12+
13+
14+
class NekoAnimatedSprite(displayio.TileGrid):
15+
# how many pixels the cat will move for each step
16+
CONFIG_STEP_SIZE = 10
17+
18+
# how likely the cat is to stop moving to clean or sleep.
19+
# lower number means more likely to happen
20+
CONFIG_STOP_CHANCE_FACTOR = 30
21+
22+
# how likely the cat is to start moving after scratching a wall.
23+
# lower number means mroe likely to heppen
24+
CONFIG_START_CHANCE_FACTOR = 10
25+
26+
# Minimum time to stop and scratch in seconds. larger time means scratch for longer
27+
CONFIG_MIN_SCRATCH_TIME = 2
28+
29+
# State object indexes
30+
_ID = 0
31+
_ANIMATION_LIST = 1
32+
_MOVEMENT_STEP = 2
33+
34+
# last time an animation occurred
35+
LAST_ANIMATION_TIME = -1
36+
37+
# index of the sprite within the current animation that is currently showing
38+
CURRENT_ANIMATION_INDEX = 0
39+
40+
# last time the cat changed states
41+
# used to enforce minimum scratch time
42+
LAST_STATE_CHANGE_TIME = -1
43+
44+
# State objects
45+
# (ID, (Animation List), (Step Sizes))
46+
STATE_SITTING = (0, (0,), (0, 0))
47+
48+
STATE_MOVING_LEFT = (1, (20, 21), (-CONFIG_STEP_SIZE, 0))
49+
STATE_MOVING_UP = (2, (16, 17), (0, -CONFIG_STEP_SIZE))
50+
STATE_MOVING_RIGHT = (3, (12, 13), (CONFIG_STEP_SIZE, 0))
51+
STATE_MOVING_DOWN = (4, (8, 9), (0, CONFIG_STEP_SIZE))
52+
STATE_MOVING_UP_RIGHT = (
53+
5,
54+
(14, 15),
55+
(CONFIG_STEP_SIZE // 2, -CONFIG_STEP_SIZE // 2),
56+
)
57+
STATE_MOVING_UP_LEFT = (
58+
6,
59+
(18, 19),
60+
(-CONFIG_STEP_SIZE // 2, -CONFIG_STEP_SIZE // 2),
61+
)
62+
STATE_MOVING_DOWN_LEFT = (
63+
7,
64+
(22, 23),
65+
(-CONFIG_STEP_SIZE // 2, CONFIG_STEP_SIZE // 2),
66+
)
67+
STATE_MOVING_DOWN_RIGHT = (
68+
8,
69+
(10, 11),
70+
(CONFIG_STEP_SIZE // 2, CONFIG_STEP_SIZE // 2),
71+
)
72+
73+
STATE_SCRATCHING_LEFT = (9, (30, 31), (0, 0))
74+
STATE_SCRATCHING_RIGHT = (10, (26, 27), (0, 0))
75+
STATE_SCRATCHING_DOWN = (11, (24, 25), (0, 0))
76+
STATE_SCRATCHING_UP = (12, (28, 29), (0, 0))
77+
78+
STATE_CLEANING = (13, (0, 0, 1, 1, 2, 3, 2, 3, 1, 1, 2, 3, 2, 3, 0, 0, 0), (0, 0))
79+
STATE_SLEEPING = (
80+
14,
81+
(
82+
0,
83+
0,
84+
4,
85+
4,
86+
4,
87+
0,
88+
0,
89+
4,
90+
4,
91+
4,
92+
0,
93+
0,
94+
5,
95+
6,
96+
5,
97+
6,
98+
5,
99+
6,
100+
5,
101+
6,
102+
5,
103+
6,
104+
7,
105+
7,
106+
0,
107+
0,
108+
0,
109+
),
110+
(0, 0),
111+
)
112+
113+
# these states count as "moving"
114+
MOVING_STATES = (
115+
STATE_MOVING_UP,
116+
STATE_MOVING_DOWN,
117+
STATE_MOVING_LEFT,
118+
STATE_MOVING_RIGHT,
119+
STATE_MOVING_UP_LEFT,
120+
STATE_MOVING_UP_RIGHT,
121+
STATE_MOVING_DOWN_LEFT,
122+
STATE_MOVING_DOWN_RIGHT,
123+
)
124+
125+
# current state private field
126+
_CURRENT_STATE = STATE_SITTING
127+
128+
# current animation list
129+
CURRENT_ANIMATION = _CURRENT_STATE[_ANIMATION_LIST]
130+
131+
"""
132+
Neko Animated Cat Sprite. Extends displayio.TileGrid manages changing the visible
133+
sprite image to animate the cat in it's various states.
134+
135+
:param float animation_time: How long to wait in-between animation frames. Unit is seconds.
136+
default is 0.3 seconds
137+
:param tuple display_size: Tuple containing width and height of display.
138+
Defaults to values from board.DISPLAY. Used to determine with we are at the edge
139+
so we know to start scratching.
140+
"""
141+
142+
def __init__(self, animation_time=0.3, display_size=None):
143+
if not display_size:
144+
# if display_size was not passed, try to use defaults from board
145+
if "DISPLAY" in dir(board):
146+
self._display_size = (board.DISPLAY.width, board.DISPLAY.height)
147+
else:
148+
raise RuntimeError(
149+
"Must pass display_size argument if not using built-in display."
150+
)
151+
else:
152+
# use the display_size that was passed in
153+
self._display_size = display_size
154+
155+
# Load the sprite sheet bitmap and palette
156+
sprite_sheet, palette = adafruit_imageload.load(
157+
"/neko_cat_spritesheet.bmp",
158+
bitmap=displayio.Bitmap,
159+
palette=displayio.Palette,
160+
)
161+
162+
# make the first color transparent
163+
palette.make_transparent(0)
164+
165+
# Create a sprite tilegrid as self
166+
super().__init__(
167+
sprite_sheet,
168+
pixel_shader=palette,
169+
width=1,
170+
height=1,
171+
tile_width=32,
172+
tile_height=32,
173+
)
174+
175+
self.x = 0
176+
self.y = 0
177+
178+
# set the animation time into a private field
179+
self._animation_time = animation_time
180+
181+
def _advance_animation_index(self):
182+
"""
183+
Helper function to increment the animation index, and wrap it back around to
184+
0 after it reaches the final animation in the list.
185+
:return: None
186+
"""
187+
self.CURRENT_ANIMATION_INDEX += 1
188+
if self.CURRENT_ANIMATION_INDEX >= len(self.CURRENT_ANIMATION):
189+
self.CURRENT_ANIMATION_INDEX = 0
190+
191+
@property
192+
def animation_time(self):
193+
"""
194+
How long to wait in-between animation frames. Unit is seconds.
195+
196+
:return: animation_time
197+
"""
198+
return self._animation_time
199+
200+
@animation_time.setter
201+
def animation_time(self, new_time):
202+
self._animation_time = new_time
203+
204+
@property
205+
def current_state(self):
206+
"""
207+
The current state object.
208+
(ID, (Animation List), (Step Sizes))
209+
210+
:return tuple: current state object
211+
"""
212+
return self._CURRENT_STATE
213+
214+
@current_state.setter
215+
def current_state(self, new_state):
216+
# update the current state object
217+
self._CURRENT_STATE = new_state
218+
# update the current animation list
219+
self.CURRENT_ANIMATION = new_state[self._ANIMATION_LIST]
220+
# reset current animation index to 0
221+
self.CURRENT_ANIMATION_INDEX = 0
222+
# show the first sprite in the animation
223+
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
224+
# update the last state change time
225+
self.LAST_STATE_CHANGE_TIME = time.monotonic()
226+
227+
def animate(self):
228+
"""
229+
If enough time has passed since the previous animation then
230+
execute the next animation step by changing the currently visible sprite and
231+
advancing the animation index.
232+
233+
:return bool: True if an animation frame occured. False if it's not time yet
234+
for an animation frame.
235+
"""
236+
_now = time.monotonic()
237+
# is it time to do an animation step?
238+
if _now > self.LAST_ANIMATION_TIME + self.animation_time:
239+
# update the visible sprite
240+
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
241+
# advance the animation index
242+
self._advance_animation_index()
243+
# update the last animation time
244+
self.LAST_ANIMATION_TIME = _now
245+
return True
246+
247+
# Not time for animation step yet
248+
return False
249+
250+
@property
251+
def is_moving(self):
252+
"""
253+
Is the cat currently moving or not.
254+
255+
:return bool: True if cat is in a moving state. False otherwise.
256+
"""
257+
return self.current_state in self.MOVING_STATES
258+
259+
def update(self):
260+
# pylint: disable=too-many-branches
261+
"""
262+
Attempt to do animation step. Move if in a moving state. Change states if needed.
263+
264+
:return: None
265+
"""
266+
_now = time.monotonic()
267+
# attempt animation
268+
did_animate = self.animate()
269+
270+
# if we did do an animation step
271+
if did_animate:
272+
# if cat is in a moving state
273+
if self.is_moving:
274+
# random chance to start sleeping or cleaning
275+
_roll = random.randint(0, self.CONFIG_STOP_CHANCE_FACTOR - 1)
276+
if _roll == 0:
277+
# change to new state: sleeping or cleaning
278+
_chosen_state = random.choice(
279+
(self.STATE_CLEANING, self.STATE_SLEEPING)
280+
)
281+
self.current_state = _chosen_state
282+
else: # cat is not moving
283+
284+
# if we are currently in a scratching state
285+
if len(self.current_state[self._ANIMATION_LIST]) <= 2:
286+
287+
# check if we have scratched the minimum time
288+
if (
289+
_now
290+
>= self.LAST_STATE_CHANGE_TIME + self.CONFIG_MIN_SCRATCH_TIME
291+
):
292+
# minimum scratch time has elapsed
293+
294+
# random chance to start moving
295+
_roll = random.randint(0, self.CONFIG_START_CHANCE_FACTOR - 1)
296+
if _roll == 0:
297+
# start moving in a random direction
298+
_chosen_state = random.choice(self.MOVING_STATES)
299+
self.current_state = _chosen_state
300+
301+
else: # if we are sleeping or cleaning or another complex animation state
302+
303+
# if we have done every step of the animation
304+
if self.CURRENT_ANIMATION_INDEX == 0:
305+
# change to a random moving state
306+
_chosen_state = random.choice(self.MOVING_STATES)
307+
self.current_state = _chosen_state
308+
309+
# If we are far enough away from side walls to step in the current moving direction
310+
if (
311+
0
312+
<= (self.x + self.current_state[self._MOVEMENT_STEP][0])
313+
< (self._display_size[0] - self.tile_width)
314+
):
315+
316+
# move the cat horizontally by current state step size x
317+
self.x += self.current_state[self._MOVEMENT_STEP][0]
318+
319+
else: # we ran into a side wall
320+
if self.x > self.CONFIG_STEP_SIZE:
321+
# ran into right wall
322+
self.x = self._display_size[0] - self.tile_width - 1
323+
# change state to scratching right
324+
self.current_state = self.STATE_SCRATCHING_RIGHT
325+
else:
326+
# ran into left wall
327+
self.x = 1
328+
# change state to scratching left
329+
self.current_state = self.STATE_SCRATCHING_LEFT
330+
331+
# If we are far enough away from top and bottom walls
332+
# to step in the current moving direction
333+
if (
334+
0
335+
<= (self.y + self.current_state[self._MOVEMENT_STEP][1])
336+
< (self._display_size[1] - self.tile_height)
337+
):
338+
339+
# move the cat vertically by current state step size y
340+
self.y += self.current_state[self._MOVEMENT_STEP][1]
341+
342+
else: # ran into top or bottom wall
343+
if self.y > self.CONFIG_STEP_SIZE:
344+
# ran into bottom wall
345+
self.y = self._display_size[1] - self.tile_height - 1
346+
# change state to scratching down
347+
self.current_state = self.STATE_SCRATCHING_DOWN
348+
else:
349+
# ran into top wall
350+
self.y = 1
351+
# change state to scratching up
352+
self.current_state = self.STATE_SCRATCHING_UP
353+
354+
355+
# default to built-in display
356+
display = board.DISPLAY
357+
358+
# create displayio Group
359+
main_group = displayio.Group()
360+
361+
# create background group
362+
background_group = displayio.Group(scale=16)
363+
background_bitmap = displayio.Bitmap(20, 15, 1)
364+
background_palette = displayio.Palette(1)
365+
background_palette[0] = BACKGROUND_COLOR
366+
background_tilegrid = displayio.TileGrid(
367+
background_bitmap, pixel_shader=background_palette
368+
)
369+
background_group.append(background_tilegrid)
370+
371+
# add background to main_group
372+
main_group.append(background_group)
373+
374+
# create Neko
375+
neko = NekoAnimatedSprite(animation_time=ANIMATION_TIME)
376+
377+
# put Neko in center of display
378+
neko.x = display.width // 2 - neko.tile_width // 2
379+
neko.y = display.height // 2 - neko.tile_height // 2
380+
381+
# add neko to main_group
382+
main_group.append(neko)
383+
384+
# show main_group on the display
385+
display.show(main_group)
386+
387+
while True:
388+
# update Neko to do animations and movements
389+
neko.update()
24.2 KB
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SPDX-FileCopyrightText: GoodClover
2+
3+
SPDX-License-Identifier: Public Domain

0 commit comments

Comments
 (0)