├── .travis.yml ├── LICENSE ├── README.md ├── demo1.cc ├── demo2.cc ├── fsm.cpp └── fsm.hpp /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | compiler: 4 | - clang 5 | - gcc 6 | 7 | install: 8 | - wget --quiet -O - https://raw.githubusercontent.com/r-lyeh/depot/master/travis.pre.sh | bash -x 9 | 10 | script: 11 | - wget --quiet -O - https://raw.githubusercontent.com/r-lyeh/depot/master/travis.build.sh | bash -x 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 r-lyeh (https://github.com/r-lyeh) 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fsm 2 | === 3 | 4 | FSM is a lightweight finite-state machine class. Both Hierarchical FSM and simple FSM implementations are provided. 5 | 6 | ### Features 7 | - [x] Expressive. Basic usage around `on(state,trigger) -> do lambda` expression. 8 | - [x] Tiny, cross-platform, stand-alone, header-only. 9 | - [x] ZLIB/libPNG licensed. 10 | 11 | ### Links 12 | [Finite-State Machines: Theory and Implementation](http://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867) 13 | 14 | ### Showcase 15 | ```c++ 16 | // basic hfsm sample 17 | 18 | #include 19 | #include "fsm.hpp" 20 | 21 | // custom states (gerunds) and actions (infinitives) 22 | 23 | enum { 24 | walking = 'WALK', 25 | defending = 'DEFN', 26 | 27 | tick = 'tick', 28 | }; 29 | 30 | struct ant_t { 31 | fsm::stack fsm; 32 | int health, distance, flow; 33 | 34 | ant_t() : health(0), distance(0), flow(1) { 35 | // define fsm transitions: on(state,trigger) -> do lambda 36 | fsm.on(walking, 'init') = [&]( const fsm::args &args ) { 37 | std::cout << "initializing" << std::endl; 38 | }; 39 | fsm.on(walking, 'quit') = [&]( const fsm::args &args ) { 40 | std::cout << "exiting" << std::endl; 41 | }; 42 | fsm.on(walking, 'push') = [&]( const fsm::args &args ) { 43 | std::cout << "pushing current task." << std::endl; 44 | }; 45 | fsm.on(walking, 'back') = [&]( const fsm::args &args ) { 46 | std::cout << "back from another task. remaining distance: " << distance << std::endl; 47 | }; 48 | fsm.on(walking, tick) = [&]( const fsm::args &args ) { 49 | std::cout << "\r" << "\\|/-"[ distance % 4 ] << " walking " << (flow > 0 ? "-->" : "<--") << " "; 50 | distance += flow; 51 | if( 1000 == distance ) { 52 | std::cout << "at food!" << std::endl; 53 | flow = -flow; 54 | } 55 | if( -1000 == distance ) { 56 | std::cout << "at home!" << std::endl; 57 | flow = -flow; 58 | } 59 | }; 60 | fsm.on(defending, 'init') = [&]( const fsm::args &args ) { 61 | health = 1000; 62 | std::cout << "somebody is attacking me! he has " << health << " health points" << std::endl; 63 | }; 64 | fsm.on(defending, tick) = [&]( const fsm::args &args ) { 65 | std::cout << "\r" << "\\|/-$"[ health % 4 ] << " health: (" << health << ") "; 66 | --health; 67 | if( health < 0 ) { 68 | std::cout << std::endl; 69 | fsm.pop(); 70 | } 71 | }; 72 | 73 | // set initial fsm state 74 | fsm.set( walking ); 75 | } 76 | }; 77 | 78 | int main() { 79 | ant_t ant; 80 | for(int i = 0; i < 12000; ++i) { 81 | if( 0 == rand() % 10000 ) { 82 | ant.fsm.push(defending); 83 | } 84 | ant.fsm.command(tick); 85 | } 86 | } 87 | ``` 88 | 89 | ### Changelog 90 | - v1.0.0 (2015/11/29): Code revisited to use fourcc integers (much faster); clean ups suggested by Chang Qian 91 | - v0.0.0 (2014/02/15): Initial version 92 | -------------------------------------------------------------------------------- /demo1.cc: -------------------------------------------------------------------------------- 1 | #define FSM_BUILD_SAMPLE1 2 | #include "fsm.hpp" 3 | 4 | -------------------------------------------------------------------------------- /demo2.cc: -------------------------------------------------------------------------------- 1 | #define FSM_BUILD_SAMPLE2 2 | #include "fsm.hpp" 3 | 4 | -------------------------------------------------------------------------------- /fsm.cpp: -------------------------------------------------------------------------------- 1 | #include "fsm.hpp" 2 | -------------------------------------------------------------------------------- /fsm.hpp: -------------------------------------------------------------------------------- 1 | // Simple FSM/HFSM class 2 | // - rlyeh [2011..2015], zlib/libpng licensed. 3 | 4 | // [ref] http://en.wikipedia.org/wiki/Finite-state_machine 5 | // [todo] GOAP? behavior trees? 6 | // [todo] counters 7 | // [note] common actions are 'init', 'quit', 'push', 'back' (integers) 8 | // - init and quit are called everytime a state is created or destroyed. 9 | // - push and back are called everytime a state is paused or resumed. Ie, when pushing and popping the stack tree. 10 | // [note] on child states (tree of fsm's): 11 | // - actions are handled to the most inner active state in the decision tree 12 | // - unhandled actions are delegated to the parent state handler until handled or discarded by root state 13 | 14 | #pragma once 15 | 16 | #define FSM_VERSION "1.0.0" /* (2015/11/29) Code revisited to use fourcc integers (much faster); clean ups suggested by Chang Qian 17 | #define FSM_VERSION "0.0.0" // (2014/02/15) Initial version */ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | namespace fsm 29 | { 30 | template 31 | inline std::string to_string( const T &t ) { 32 | std::stringstream ss; 33 | return ss << t ? ss.str() : std::string(); 34 | } 35 | 36 | template<> 37 | inline std::string to_string( const std::string &t ) { 38 | return t; 39 | } 40 | 41 | typedef std::vector args; 42 | typedef std::function< void( const fsm::args &args ) > call; 43 | 44 | struct state { 45 | int name; 46 | fsm::args args; 47 | 48 | state( const int &name = 'null' ) : name(name) 49 | {} 50 | 51 | state operator()() const { 52 | state self = *this; 53 | self.args = {}; 54 | return self; 55 | } 56 | template 57 | state operator()( const T0 &t0 ) const { 58 | state self = *this; 59 | self.args = { fsm::to_string(t0) }; 60 | return self; 61 | } 62 | template 63 | state operator()( const T0 &t0, const T1 &t1 ) const { 64 | state self = *this; 65 | self.args = { fsm::to_string(t0), fsm::to_string(t1) }; 66 | return self; 67 | } 68 | 69 | operator int () const { 70 | return name; 71 | } 72 | 73 | bool operator<( const state &other ) const { 74 | return name < other.name; 75 | } 76 | bool operator==( const state &other ) const { 77 | return name == other.name; 78 | } 79 | 80 | template 81 | inline friend ostream &operator<<( ostream &out, const state &t ) { 82 | if( t.name >= 256 ) { 83 | out << char((t.name >> 24) & 0xff); 84 | out << char((t.name >> 16) & 0xff); 85 | out << char((t.name >> 8) & 0xff); 86 | out << char((t.name >> 0) & 0xff); 87 | } else { 88 | out << t.name; 89 | } 90 | out << "("; 91 | std::string sep; 92 | for(auto &arg : t.args ) { 93 | out << sep << arg; 94 | sep = ','; 95 | } 96 | out << ")"; 97 | return out; 98 | } 99 | }; 100 | 101 | typedef state trigger; 102 | 103 | struct transition { 104 | fsm::state previous, trigger, current; 105 | 106 | template 107 | inline friend ostream &operator<<( ostream &out, const transition &t ) { 108 | out << t.previous << " -> " << t.trigger << " -> " << t.current; 109 | return out; 110 | } 111 | }; 112 | 113 | class stack { 114 | public: 115 | 116 | stack( const fsm::state &start = 'null' ) : deque(1) { 117 | deque[0] = start; 118 | call( deque.back(), 'init' ); 119 | } 120 | 121 | stack( int start ) : stack( fsm::state(start) ) 122 | {} 123 | 124 | ~stack() { 125 | // ensure state destructors are called (w/ 'quit') 126 | while( size() ) { 127 | pop(); 128 | } 129 | } 130 | 131 | // pause current state (w/ 'push') and create a new active child (w/ 'init') 132 | void push( const fsm::state &state ) { 133 | if( deque.size() && deque.back() == state ) { 134 | return; 135 | } 136 | // queue 137 | call( deque.back(), 'push' ); 138 | deque.push_back( state ); 139 | call( deque.back(), 'init' ); 140 | } 141 | 142 | // terminate current state and return to parent (if any) 143 | void pop() { 144 | if( deque.size() ) { 145 | call( deque.back(), 'quit' ); 146 | deque.pop_back(); 147 | } 148 | if( deque.size() ) { 149 | call( deque.back(), 'back' ); 150 | } 151 | } 152 | 153 | // set current active state 154 | void set( const fsm::state &state ) { 155 | if( deque.size() ) { 156 | replace( deque.back(), state ); 157 | } else { 158 | push(state); 159 | } 160 | } 161 | 162 | // number of children (stack) 163 | size_t size() const { 164 | return deque.size(); 165 | } 166 | 167 | // info 168 | // [] classic behaviour: "hello"[5] = undefined, "hello"[-1] = undefined 169 | // [] extended behaviour: "hello"[5] = h, "hello"[-1] = o, "hello"[-2] = l 170 | fsm::state get_state( signed pos = -1 ) const { 171 | signed size = (signed)(deque.size()); 172 | return size ? *( deque.begin() + (pos >= 0 ? pos % size : size - 1 + ((pos+1) % size) ) ) : fsm::state(); 173 | } 174 | fsm::transition get_log( signed pos = -1 ) const { 175 | signed size = (signed)(log.size()); 176 | return size ? *( log.begin() + (pos >= 0 ? pos % size : size - 1 + ((pos+1) % size) ) ) : fsm::transition(); 177 | } 178 | std::string get_trigger() const { 179 | std::stringstream ss; 180 | return ss << current_trigger, ss.str(); 181 | } 182 | 183 | bool is_state( const fsm::state &state ) const { 184 | return deque.empty() ? false : ( deque.back() == state ); 185 | } 186 | 187 | /* (idle)___(trigger)__/''(hold)''''(release)''\__ 188 | bool is_idle() const { return transition.previous == transition.current; } 189 | bool is_triggered() const { return transition.previous == transition.current; } 190 | bool is_hold() const { return transition.previous == transition.current; } 191 | bool is_released() const { return transition.previous == transition.current; } */ 192 | 193 | // setup 194 | fsm::call &on( const fsm::state &from, const fsm::state &to ) { 195 | return callbacks[ bistate(from,to) ]; 196 | } 197 | 198 | // generic call 199 | bool call( const fsm::state &from, const fsm::state &to ) const { 200 | std::map< bistate, fsm::call >::const_iterator found = callbacks.find(bistate(from,to)); 201 | if( found != callbacks.end() ) { 202 | log.push_back( { from, current_trigger, to } ); 203 | if( log.size() > 50 ) { 204 | log.pop_front(); 205 | } 206 | found->second( to.args ); 207 | return true; 208 | } 209 | return false; 210 | } 211 | 212 | // user commands 213 | bool command( const fsm::state &trigger ) { 214 | size_t size = this->size(); 215 | if( !size ) { 216 | return false; 217 | } 218 | current_trigger = fsm::state(); 219 | std::deque< states::reverse_iterator > aborted; 220 | for( auto it = deque.rbegin(); it != deque.rend(); ++it ) { 221 | fsm::state &self = *it; 222 | if( !call(self,trigger) ) { 223 | aborted.push_back(it); 224 | continue; 225 | } 226 | for( auto it = aborted.begin(), end = aborted.end(); it != end; ++it ) { 227 | call(**it, 'quit'); 228 | deque.erase(--(it->base())); 229 | } 230 | current_trigger = trigger; 231 | return true; 232 | } 233 | return false; 234 | } 235 | template 236 | bool command( const fsm::state &trigger, const T &arg1 ) { 237 | return command( trigger(arg1) ); 238 | } 239 | template 240 | bool command( const fsm::state &trigger, const T &arg1, const U &arg2 ) { 241 | return command( trigger(arg1, arg2) ); 242 | } 243 | 244 | // debug 245 | template 246 | ostream &debug( ostream &out ) { 247 | int total = log.size(); 248 | out << "status {" << std::endl; 249 | std::string sep = "\t"; 250 | for( states::const_reverse_iterator it = deque.rbegin(), end = deque.rend(); it != end; ++it ) { 251 | out << sep << *it; 252 | sep = " -> "; 253 | } 254 | out << std::endl; 255 | out << "} log (" << total << " entries) {" << std::endl; 256 | for( int i = 0 ; i < total; ++i ) { 257 | out << "\t" << log[i] << std::endl; 258 | } 259 | out << "}" << std::endl; 260 | return out; 261 | } 262 | 263 | // aliases 264 | bool operator()( const fsm::state &trigger ) { 265 | return command( trigger ); 266 | } 267 | template 268 | bool operator()( const fsm::state &trigger, const T &arg1 ) { 269 | return command( trigger(arg1) ); 270 | } 271 | template 272 | bool operator()( const fsm::state &trigger, const T &arg1, const U &arg2 ) { 273 | return command( trigger(arg1, arg2) ); 274 | } 275 | template 276 | inline friend ostream &operator<<( ostream &out, const stack &t ) { 277 | return t.debug( out ), out; 278 | } 279 | 280 | protected: 281 | 282 | void replace( fsm::state ¤t, const fsm::state &next ) { 283 | call( current, 'quit' ); 284 | current = next; 285 | call( current, 'init' ); 286 | } 287 | 288 | typedef std::pair bistate; 289 | std::map< bistate, fsm::call > callbacks; 290 | 291 | mutable std::deque< fsm::transition > log; 292 | std::deque< fsm::state > deque; 293 | fsm::state current_trigger; 294 | 295 | typedef std::deque< fsm::state > states; 296 | }; 297 | } 298 | 299 | #ifdef FSM_BUILD_SAMPLE1 300 | 301 | // basic fsm, CD player sample 302 | 303 | #include 304 | 305 | // custom states (gerunds) and actions (infinitives) 306 | 307 | enum { 308 | opening, 309 | closing, 310 | waiting, 311 | playing, 312 | 313 | open, 314 | close, 315 | play, 316 | stop, 317 | insert, 318 | eject, 319 | }; 320 | 321 | struct cd_player { 322 | 323 | // implementation variables 324 | bool has_cd; 325 | 326 | // implementation conditions / guards 327 | bool good_disk_format() { return true; } 328 | 329 | // implementation actions 330 | void open_tray() { std::cout << "opening tray" << std::endl; } 331 | void close_tray() { std::cout << "closing tray" << std::endl; } 332 | void get_cd_info() { std::cout << "retrieving CD info" << std::endl; } 333 | void start_playback( const std::string &track ) { std::cout << "playing track #" << track << std::endl; } 334 | 335 | // the core 336 | fsm::stack fsm; 337 | 338 | cd_player() : has_cd(false) 339 | { 340 | // define fsm transitions: on(state,trigger) -> do lambda 341 | fsm.on(opening,close) = [&]( const fsm::args &args ) { 342 | close_tray(); 343 | if( !has_cd ) { 344 | fsm.set( closing ); 345 | } else { 346 | get_cd_info(); 347 | fsm.set( waiting ); 348 | } 349 | }; 350 | fsm.on(opening,insert) = [&]( const fsm::args &args ) { 351 | has_cd = true; 352 | fsm.set( opening ); 353 | }; 354 | fsm.on(opening,eject) = [&]( const fsm::args &args ) { 355 | has_cd = false; 356 | fsm.set( opening ); 357 | }; 358 | 359 | fsm.on(closing,open) = [&]( const fsm::args &args ) { 360 | open_tray(); 361 | fsm.set( opening ); 362 | }; 363 | 364 | fsm.on(waiting,play) = [&]( const fsm::args &args ) { 365 | if( !good_disk_format() ) { 366 | fsm.set( waiting ); 367 | } else { 368 | start_playback( args[0] ); 369 | fsm.set( playing ); 370 | } 371 | }; 372 | fsm.on(waiting,open) = [&]( const fsm::args &args ) { 373 | open_tray(); 374 | fsm.set( opening ); 375 | }; 376 | 377 | fsm.on(playing,open) = [&]( const fsm::args &args ) { 378 | open_tray(); 379 | fsm.set( opening ); 380 | }; 381 | fsm.on(playing,stop) = [&]( const fsm::args &args ) { 382 | fsm.set( waiting ); 383 | }; 384 | 385 | // set initial fsm state 386 | fsm.set(opening); 387 | } 388 | }; 389 | 390 | // usage 391 | 392 | int main() { 393 | cd_player cd; 394 | 395 | for(;;) { 396 | std::cout << "[" << cd.fsm.get_state() << "] "; 397 | std::cout << "(o)pen lid/(c)lose lid, (i)nsert cd/(e)ject cd, (p)lay/(s)top cd? "; 398 | 399 | char cmd; 400 | std::cin >> cmd; 401 | 402 | switch( cmd ) { 403 | case 'p': cd.fsm.command(play,1+rand()%10); break; 404 | case 'o': cd.fsm.command(open); break; 405 | case 'c': cd.fsm.command(close); break; 406 | case 's': cd.fsm.command(stop); break; 407 | case 'i': cd.fsm.command(insert); break; 408 | case 'e': cd.fsm.command(eject); break; 409 | default : std::cout << "what?" << std::endl; 410 | } 411 | } 412 | } 413 | 414 | #endif 415 | 416 | #ifdef FSM_BUILD_SAMPLE2 417 | 418 | // basic hfsm sample 419 | 420 | #include 421 | 422 | // custom states (gerunds) and actions (infinitives) 423 | 424 | enum { 425 | walking = 'WALK', 426 | defending = 'DEFN', 427 | 428 | tick = 'tick', 429 | }; 430 | 431 | struct ant_t { 432 | fsm::stack fsm; 433 | int health, distance, flow; 434 | 435 | ant_t() : health(0), distance(0), flow(1) { 436 | // define fsm transitions: on(state,trigger) -> do lambda 437 | fsm.on(walking, 'init') = [&]( const fsm::args &args ) { 438 | std::cout << "initializing" << std::endl; 439 | }; 440 | fsm.on(walking, 'quit') = [&]( const fsm::args &args ) { 441 | std::cout << "exiting" << std::endl; 442 | }; 443 | fsm.on(walking, 'push') = [&]( const fsm::args &args ) { 444 | std::cout << "pushing current task." << std::endl; 445 | }; 446 | fsm.on(walking, 'back') = [&]( const fsm::args &args ) { 447 | std::cout << "back from another task. remaining distance: " << distance << std::endl; 448 | }; 449 | fsm.on(walking, tick) = [&]( const fsm::args &args ) { 450 | std::cout << "\r" << "\\|/-"[ distance % 4 ] << " walking " << (flow > 0 ? "-->" : "<--") << " "; 451 | distance += flow; 452 | if( 1000 == distance ) { 453 | std::cout << "at food!" << std::endl; 454 | flow = -flow; 455 | } 456 | if( -1000 == distance ) { 457 | std::cout << "at home!" << std::endl; 458 | flow = -flow; 459 | } 460 | }; 461 | fsm.on(defending, 'init') = [&]( const fsm::args &args ) { 462 | health = 1000; 463 | std::cout << "somebody is attacking me! he has " << health << " health points" << std::endl; 464 | }; 465 | fsm.on(defending, tick) = [&]( const fsm::args &args ) { 466 | std::cout << "\r" << "\\|/-$"[ health % 4 ] << " health: (" << health << ") "; 467 | --health; 468 | if( health < 0 ) { 469 | std::cout << std::endl; 470 | fsm.pop(); 471 | } 472 | }; 473 | 474 | // set initial fsm state 475 | fsm.set( walking ); 476 | } 477 | }; 478 | 479 | int main() { 480 | ant_t ant; 481 | for(int i = 0; i < 12000; ++i) { 482 | if( 0 == rand() % 10000 ) { 483 | ant.fsm.push(defending); 484 | } 485 | ant.fsm.command(tick); 486 | } 487 | } 488 | 489 | #endif 490 | --------------------------------------------------------------------------------