Skip to content

Commit e3ad67f

Browse files
committed
adding circuitpython neko cat
1 parent 99a7e2d commit e3ad67f

File tree

3 files changed

+389
-0
lines changed

3 files changed

+389
-0
lines changed

CircuitPython_Neko_Cat/code.py

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
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+
# set the animation time into a private field
176+
self._animation_time = animation_time
177+
178+
def _advance_animation_index(self):
179+
"""
180+
Helper function to increment the animation index, and wrap it back around to
181+
0 after it reaches the final animation in the list.
182+
:return: None
183+
"""
184+
self.CURRENT_ANIMATION_INDEX += 1
185+
if self.CURRENT_ANIMATION_INDEX >= len(self.CURRENT_ANIMATION):
186+
self.CURRENT_ANIMATION_INDEX = 0
187+
188+
@property
189+
def animation_time(self):
190+
"""
191+
How long to wait in-between animation frames. Unit is seconds.
192+
193+
:return: animation_time
194+
"""
195+
return self._animation_time
196+
197+
@animation_time.setter
198+
def animation_time(self, new_time):
199+
self._animation_time = new_time
200+
201+
@property
202+
def current_state(self):
203+
"""
204+
The current state object.
205+
(ID, (Animation List), (Step Sizes))
206+
207+
:return tuple: current state object
208+
"""
209+
return self._CURRENT_STATE
210+
211+
@current_state.setter
212+
def current_state(self, new_state):
213+
# update the current state object
214+
self._CURRENT_STATE = new_state
215+
# update the current animation list
216+
self.CURRENT_ANIMATION = new_state[self._ANIMATION_LIST]
217+
# reset current animation index to 0
218+
self.CURRENT_ANIMATION_INDEX = 0
219+
# show the first sprite in the animation
220+
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
221+
# update the last state change time
222+
self.LAST_STATE_CHANGE_TIME = time.monotonic()
223+
224+
def animate(self):
225+
"""
226+
If enough time has passed since the previous animation then
227+
execute the next animation step by changing the currently visible sprite and
228+
advancing the animation index.
229+
230+
:return bool: True if an animation frame occured. False if it's not time yet
231+
for an animation frame.
232+
"""
233+
_now = time.monotonic()
234+
# is it time to do an animation step?
235+
if _now > self.LAST_ANIMATION_TIME + self.animation_time:
236+
# update the visible sprite
237+
self[0] = self.CURRENT_ANIMATION[self.CURRENT_ANIMATION_INDEX]
238+
# advance the animation index
239+
self._advance_animation_index()
240+
# update the last animation time
241+
self.LAST_ANIMATION_TIME = _now
242+
return True
243+
244+
# Not time for animation step yet
245+
return False
246+
247+
@property
248+
def is_moving(self):
249+
"""
250+
Is the cat currently moving or not.
251+
252+
:return bool: True if cat is in a moving state. False otherwise.
253+
"""
254+
return self.current_state in self.MOVING_STATES
255+
256+
def update(self):
257+
# pylint: disable=too-many-branches
258+
"""
259+
Attempt to do animation step. Move if in a moving state. Change states if needed.
260+
261+
:return: None
262+
"""
263+
_now = time.monotonic()
264+
# attempt animation
265+
did_animate = self.animate()
266+
267+
# if we did do an animation step
268+
if did_animate:
269+
# if cat is in a moving state
270+
if self.is_moving:
271+
# random chance to start sleeping or cleaning
272+
_roll = random.randint(0, self.CONFIG_STOP_CHANCE_FACTOR - 1)
273+
if _roll == 0:
274+
# change to new state: sleeping or cleaning
275+
_chosen_state = random.choice(
276+
(self.STATE_CLEANING, self.STATE_SLEEPING)
277+
)
278+
self.current_state = _chosen_state
279+
else: # cat is not moving
280+
281+
# if we are currently in a scratching state
282+
if len(self.current_state[self._ANIMATION_LIST]) <= 2:
283+
284+
# check if we have scratched the minimum time
285+
if (
286+
_now
287+
>= self.LAST_STATE_CHANGE_TIME + self.CONFIG_MIN_SCRATCH_TIME
288+
):
289+
# minimum scratch time has elapsed
290+
291+
# random chance to start moving
292+
_roll = random.randint(0, self.CONFIG_START_CHANCE_FACTOR - 1)
293+
if _roll == 0:
294+
# start moving in a random direction
295+
_chosen_state = random.choice(self.MOVING_STATES)
296+
self.current_state = _chosen_state
297+
298+
else: # if we are sleeping or cleaning or another complex animation state
299+
300+
# if we have done every step of the animation
301+
if self.CURRENT_ANIMATION_INDEX == 0:
302+
# change to a random moving state
303+
_chosen_state = random.choice(self.MOVING_STATES)
304+
self.current_state = _chosen_state
305+
306+
# If we are far enough away from side walls to step in the current moving direction
307+
if (
308+
0
309+
<= (self.x + self.current_state[self._MOVEMENT_STEP][0])
310+
< (self._display_size[0] - self.tile_width)
311+
):
312+
313+
# move the cat horizontally by current state step size x
314+
self.x += self.current_state[self._MOVEMENT_STEP][0]
315+
316+
else: # we ran into a side wall
317+
if self.x > self.CONFIG_STEP_SIZE:
318+
# ran into right wall
319+
self.x = self._display_size[0] - self.tile_width - 1
320+
# change state to scratching right
321+
self.current_state = self.STATE_SCRATCHING_RIGHT
322+
else:
323+
# ran into left wall
324+
self.x = 1
325+
# change state to scratching left
326+
self.current_state = self.STATE_SCRATCHING_LEFT
327+
328+
# If we are far enough away from top and bottom walls
329+
# to step in the current moving direction
330+
if (
331+
0
332+
<= (self.y + self.current_state[self._MOVEMENT_STEP][1])
333+
< (self._display_size[1] - self.tile_height)
334+
):
335+
336+
# move the cat vertically by current state step size y
337+
self.y += self.current_state[self._MOVEMENT_STEP][1]
338+
339+
else: # ran into top or bottom wall
340+
if self.y > self.CONFIG_STEP_SIZE:
341+
# ran into bottom wall
342+
self.y = self._display_size[1] - self.tile_height - 1
343+
# change state to scratching down
344+
self.current_state = self.STATE_SCRATCHING_DOWN
345+
else:
346+
# ran into top wall
347+
self.y = 1
348+
# change state to scratching up
349+
self.current_state = self.STATE_SCRATCHING_UP
350+
351+
352+
# default to built-in display
353+
display = board.DISPLAY
354+
355+
# create displayio Group
356+
main_group = displayio.Group()
357+
358+
# create background group
359+
background_group = displayio.Group(scale=16)
360+
background_bitmap = displayio.Bitmap(20, 15, 1)
361+
background_palette = displayio.Palette(1)
362+
background_palette[0] = BACKGROUND_COLOR
363+
background_tilegrid = displayio.TileGrid(
364+
background_bitmap, pixel_shader=background_palette
365+
)
366+
background_group.append(background_tilegrid)
367+
368+
# add background to main_group
369+
main_group.append(background_group)
370+
371+
# create Neko
372+
neko = NekoAnimatedSprite(animation_time=ANIMATION_TIME)
373+
374+
# put Neko in center of display
375+
neko.x = display.width // 2 - neko.tile_width // 2
376+
neko.y = display.height // 2 - neko.tile_height // 2
377+
378+
# add neko to main_group
379+
main_group.append(neko)
380+
381+
# show main_group on the display
382+
display.show(main_group)
383+
384+
while True:
385+
# update Neko to do animations and movements
386+
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)