Collaborated with AI to develop a Snake game

Hello everyone,

Using the LED panel matrix scene shared by Amjavi6 as inspiration, I created a secondary development project by expanding it into a 16×16 matrix. I then collaborated with AI to develop a Snake game on top of it.

Software used:

  1. Factory I/O
  2. TIA Portal
  3. PLCSIM Advanced

PLC programming language:

  • SCL (Structured Control Language)

貪食蛇.factoryio (431.2 KB)

1 Like

@bob.chen Super cool project :wink:

Hi Bob Chen:

Now, when what you mentioned is working, post the video here so I can see it. Among other things, I highly doubt that project can be done with SCL, and it must be very complex to do it with only the 136 LEDs on your panel. It’s much easier for it to solve a 6x6 Sudoku, or even make the moves in a chess game. I’m looking forward to your video of the snake working…

I should mention that adapting that game to a Factory.io scene doesn’t seem like the best thing to suggest to students, in my opinion, but don’t take my word for it; stranger things have happened.

Regards

Hi, Amjavi6,

I cannot upload the SCL file directly here, so please share your email address and I can send you the original source file.

This project was developed through human-AI collaboration. The main point is not the Snake game itself, but rather using AI collaboration to first develop working code, and then reverse-learn the problem-solving process from that working example. This allows students to quickly acquire more skills by learning from executable code.

That is also why I shared my development process with ChatGPT. You can clearly see the interaction between me and the AI throughout the entire workflow. Through this interaction, I believe PLC engineers in the AI era should improve their Prompt skills.

Therefore, my concept is that PLC is evolving from Programming Logic Control into Prompt Logic Control.

In the past, experts were created through 10,000 hours of practice.
In the future, experts may be created through 10,000 iterations.

貪食蛇.factoryio (431.2 KB)

Hi Bob Chen:

Your presentation of the Snake game is very good, but pay attention to what I’m about to say:

First of all, I don’t use Siemens, so I can’t see your files.

Second of all, it’s obvious that your project isn’t finished. Right now, it’s just doing a short cycle of certain lights on your LEDs. Obviously, if the snake finishes, the board has to start from the other side, and we also don’t know if at that speed it would have enough time to scan all the bits and indicators without crashing due to the high scan cycle.

Since you’ve shown interest in the topic, and as you can see, I did this 5 years ago on my channel, what I’m going to do is ADAPT what I already did to a PLC so that your board works. BUT, for now, I’m going to do it in a very simple way, very simple, so that you understand how to make that game in FACTORY IO.

Thirdly, when you see what I’m going to do, you’ll understand that SCL can’t do what I’m going to show you with VB.NET. A typical student can’t do it either, but that doesn’t matter to us. Since it’s already happened, we’re going to see it through to the end.

This means that if I manage to create the Snake game in Factory.io, explaining the process here in a simple and understandable way for students, we can say that Factory.io, as it is, is much more of a simulator than I thought.

Bob Chen, I’m taking over your project. I hope you can do the same as me, since the idea was yours…

Thanks for contributing such an interesting topic.

Regards

Hi Bob Chen:

As I mentioned, I don’t know if it will work. I usually test it before uploading the project, but this time, let’s see how the idea works, and when I finish contribution 127, we’ll start…

Let’s see what the creators of Factory IO, Bruno, Adriana, etc., think. Did they think their simulator was suitable for gaming?

It has solved Sudokus, played Connect 4, played chess, built the German Enigma machine, and now the Snake game.

I repeat, I don’t know if the Snake scan cycle will be fast enough for so many bits, but hey, we’re here to play, literally.

Regards

What Students Can Learn from a PLC Snake Game in Siemens SCL

This project is more than a small PLC game. It is a practical teaching case that shows how students can move beyond basic input-output control and start thinking in terms of states, data, timing, hardware mapping, and verification.
The goal is to use Siemens SCL to control a 16x16 LED matrix and build a working Snake Game. During this process, students learn several problem-solving ideas that are not easy to discover in traditional PLC lessons.

1. Separate Logic from Hardware Mapping

A key lesson is that the game logic should not be directly tied to physical output addresses.
Inside the program, the snake uses a simple coordinate system:

X = 0 to 15
Y = 0 to 15
X = 0 means left
X = 15 means right
Y = 0 means top
Y = 15 means bottom

However, the real LED matrix is controlled by Siemens output words, bytes, and bits. For example, the first row is mapped from:

Q0.0, Q0.1, Q0.2, Q0.3, Q0.4, Q0.5, Q0.6, Q0.7,
Q1.0, Q1.1, Q1.2, Q1.3, Q1.4, Q1.5, Q1.6, Q1.7

Therefore, the program needs a mapping layer:

IF #SnakeX[#i] <= 7 THEN
    #BitIndex := #SnakeX[#i] + 8;
ELSE
    #BitIndex := #SnakeX[#i] - 8;
END_IF;

The important engineering idea is:
The logical model and the physical I/O mapping should be separated.
This is useful not only for LED matrices, but also for Modbus registers, PROFINET I/O mapping, HMI tags, SCADA tags, and byte-order problems.

2. Use a Layered Program Structure

The Snake Game can be divided into three layers:

1. Game logic
   Direction, snake body, food, collision, score
2. Timing control
   GameTimer, GameTick, MoveTime
3. Display output
   LedBuffer, BitIndex, LedRow_00 to LedRow_15

This teaches students not to mix all conditions and outputs together.
A good design flow is:

Read input
-> update direction
-> wait for GameTick
-> calculate next position
-> check collision
-> update snake body
-> generate LED buffer
-> write LED output

This is a major step beyond simple PLC logic such as:

Condition is true -> Output ON
Condition is false -> Output OFF

3. Use a State Machine Instead of Only Boolean Logic

The game has several operating states:

Idle
Running
Game Over
Win
Restart

Therefore, the program uses a CASE-based state machine:

CASE #State OF
    10:
        // Idle
    20:
        // Running
    30:
        // Game Over
    40:
        // Win
END_CASE;

This teaches students that Boolean logic is not always enough. When a machine has steps, memory, events, and transitions, a state machine is usually a better solution.
This concept applies directly to conveyors, sorting systems, pick-and-place machines, automatic warehouses, packaging machines, and batch processes.

4. Understand Scan Cycle vs. Application Event

A PLC runs cyclically, but the snake should not move every scan. If it did, it would move too fast to observe.
The program uses a timer to generate a movement event:

#GameTimer(IN := (#State = 20), PT := #MoveTime);
#GameTick := #GameTimer.Q;

The snake only moves when GameTick becomes TRUE:

IF #GameTick THEN
    // Move snake
END_IF;

The key point is:
The PLC scan cycle is not the same as the application control cycle.
This idea is useful for communication polling, periodic sampling, HMI refresh, machine cycle timing, and step-by-step process control.

5. Use Arrays to Represent Moving Objects

The snake body is not written as hundreds of individual Boolean tags. It is stored as arrays:

SnakeX : Array[0..255] of Int;
SnakeY : Array[0..255] of Int;

The structure is:

SnakeX[0], SnakeY[0] = snake head
SnakeX[1], SnakeY[1] = first body segment
SnakeX[2], SnakeY[2] = second body segment

This teaches students how to use data structures in PLC programming.
The lesson is:
When many similar data items exist, use arrays and indexes.
This idea is very useful for product tracking, conveyor station data, recipe tables, alarm lists, warehouse locations, and production history.

6. Learn Shift Register Thinking

The snake body movement is based on a simple but powerful algorithm:

FOR #i := #BodyLimit TO 1 BY -1 DO
    #SnakeX[#i] := #SnakeX[#i - 1];
    #SnakeY[#i] := #SnakeY[#i - 1];
END_FOR;

Each body segment copies the previous segment position. Only the snake head actually moves according to the direction.
This is similar to shift-register logic in industrial automation.
It can be applied to:

Object tracking on conveyors
Station-to-station data transfer
Product status shifting
Inspection result tracking
Packaging line data movement
FIFO-style material tracking

7. Calculate the Next State Before Updating the Real State

The program does not immediately overwrite the snake position. It first calculates the next possible position:

#NewX := #SnakeX[0];
#NewY := #SnakeY[0];

Then it checks whether the new position is valid. Only after validation does the program update the real snake position.
The engineering principle is:
Calculate first, validate second, commit last.
This is useful for motion command validation, robot position checking, safety interlocks, recipe validation, and automatic sequence transitions.

8. Learn Bit Operations with SHL and OR

The LED matrix is generated using bit operations:

SHL(IN := WORD#16#0001, N := #BitIndex)

Then the bit is added into the row buffer:

#LedBuffer[#SnakeY[#i]] :=
    #LedBuffer[#SnakeY[#i]] OR SHL(IN := WORD#16#0001, N := #BitIndex);

This teaches students that a WORD can represent 16 LED points.
Instead of writing 16 separate Boolean outputs, the program uses:

WORD as a 16-bit row
SHL to generate a bit mask
OR to combine display points
LedBuffer to prepare output before writing to hardware

This is a strong example of why SCL is suitable for data-oriented PLC problems.

9. Use MOD for Position Generation

The food position is generated with a simple pseudo-random method:

#FoodX := #Seed MOD 16;
#FoodY := (#Seed / 16) MOD 16;

MOD 16 keeps the value inside the range 0 to 15.
This teaches students how to convert a changing number into a valid matrix position.
The same idea can be used for station indexing, recipe selection, device polling, buffer addressing, and circular queue logic.

10. Treat Collision Detection as Illegal State Checking

The program checks wall collision:

IF (#NewX < 0) OR (#NewX > 15) OR (#NewY < 0) OR (#NewY > 15) THEN
    #GameOver := TRUE;
END_IF;

It also checks self-collision by comparing the new head position with the snake body:

FOR #i := 0 TO #BodyLimit DO
    IF (#NewX = #SnakeX[#i]) AND (#NewY = #SnakeY[#i]) THEN
        #SelfCollision := TRUE;
    END_IF;
END_FOR;

This teaches students how to detect illegal states before updating the machine state.
In real automation, this maps to position limits, duplicate station occupancy, invalid routing, robot conflict checking, and conveyor zone conflict detection.

11. Reset Must Prevent Later Logic from Rewriting Outputs

During development, one issue appeared: Reset cleared the LED buffer, but later logic drew the snake again in the same scan.
The solution was:

IF NOT #Reset THEN
    // Draw food and snake
END_IF;

The key lesson is:
Reset logic must not only clear values. It must also prevent later logic from writing them again.
This is a common real-world PLC issue in emergency stop logic, alarm reset logic, manual-auto switching, output arbitration, and machine initialization.

12. Use Watch Table to Verify the Causal Chain

The Watch Table should not only check whether the LED turns on. It should verify the full causal chain:

DI button
-> NextDirection
-> GameTick
-> Direction
-> HeadX / HeadY
-> SnakeX / SnakeY
-> LedBuffer
-> LedRow output
-> physical LED display

Recommended Watch Table variables:

State
Direction
NextDirection
HeadX
HeadY
FoodX
FoodY
SnakeLength
Score
GameTimer.ET
GameTick
GameOver
Win
LedRow_00 to LedRow_15

This teaches students how to debug by tracing cause and effect, not just by looking at the final output.

13. SCL Solves a Different Type of Problem Than Ladder

Ladder is excellent for:

Start/stop circuits
Interlocks
Simple output logic
Safety contact logic
Basic machine conditions

But this project requires:

Arrays
Indexes
Loops
State machines
Bit shifting
Data movement
Collision checking
Coordinate mapping

These are easier to express in SCL.
The important message is:
SCL is not better because it is newer. It is better when the problem is data-oriented and algorithmic.

14. The Development Process Is More Valuable Than the Final Code

The most important learning point is the process:

Start with a simple idea
-> build a minimal version
-> test with Watch Table
-> find mapping errors
-> fix Reset behavior
-> verify movement
-> add snake body
-> add food and score
-> fix syntax errors
-> complete the final version

This teaches students that engineering is not about writing perfect code in one attempt.
Engineering is about:

Defining assumptions
Testing assumptions
Using evidence
Separating logic layers
Fixing one problem at a time
Protecting verified functions
Upgrading gradually

Summary

This Snake Game SCL project teaches much more than a game.
It helps students learn:

State machine thinking
Coordinate modeling
Array-based data structures
Timer-driven events
Bit-level output mapping
Collision detection
Data buffering
Watch Table diagnostics
Step-by-step engineering validation

These skills are not always visible in traditional PLC examples such as motor start-stop circuits, self-holding circuits, or traffic lights.
The core message is:
PLC programming is not only about turning outputs ON and OFF. It is about modeling states, data, timing, hardware mapping, and verification.
That is why this Snake Game is a strong teaching case for modern PLC education, especially when combined with Siemens SCL, Factory I/O, and AI-assisted development.

Hi, Amjavi6

Below is the complete SCL source code. Since the essence of SCL is simply a text-based file format, the code can be directly copied into a Notepad TXT file. Then, you only need to change the file extension from .txt to .scl.

For example:
xxx.txt → manually rename to xxx.scl

After that, the .scl file can be imported directly into Siemens TIA Portal.

This also allows you to clearly see how SCL is used to implement the entire Snake game control logic, while learning the underlying problem-solving methodology behind the program.

FUNCTION_BLOCK “FB_SnakeGame_16x16”
{ S7_Optimized_Access := ‘TRUE’ }
VERSION : 0.5

VAR_INPUT
Start : Bool;
Reset : Bool;
Pause : Bool;
BtnUp : Bool;
BtnDown : Bool;
BtnLeft : Bool;
BtnRight : Bool;
MoveTime : Time := T#300ms;
END_VAR

VAR_OUTPUT
LedRow_00 : Word;
LedRow_01 : Word;
LedRow_02 : Word;
LedRow_03 : Word;
LedRow_04 : Word;
LedRow_05 : Word;
LedRow_06 : Word;
LedRow_07 : Word;
LedRow_08 : Word;
LedRow_09 : Word;
LedRow_10 : Word;
LedRow_11 : Word;
LedRow_12 : Word;
LedRow_13 : Word;
LedRow_14 : Word;
LedRow_15 : Word;
State : Int;
Direction : Int;
HeadX : Int;
HeadY : Int;
FoodX : Int;
FoodY : Int;
SnakeLength : Int;
Score : Int;
GameOver : Bool;
GameTick : Bool;
Win : Bool;
BlinkCount : Int;
END_VAR

VAR
NextDirection : Int;
NewX : Int;
NewY : Int;
BitIndex : Int;
LedBuffer : Array[0..15] of Word;
GameTimer : TON;
BlinkTimer : TON;
BlinkVisible : Bool;
BlinkTime : Time;
SnakeX : Array[0..255] of Int;
SnakeY : Array[0..255] of Int;
Seed : Int;
EatFood : Bool;
SelfCollision : Bool;
FoodOnSnake : Bool;
NeedNewFood : Bool;
FoodReady : Bool;
END_VAR

VAR_TEMP
i : Int;
j : Int;
Attempt : Int;
BodyLimit : Int;
END_VAR

BEGIN // BEGIN indicates the start of writing SCL code.

IF #Reset THEN
    // Why:
    // Reset is the highest-priority command.
    // It must stop movement timing and blink timing, then rebuild a known safe state.
    RESET_TIMER(#GameTimer);
    RESET_TIMER(#BlinkTimer);
    #State := 10;
    #Direction := 1;
    #NextDirection := 1;
    #HeadX := 7;
    #HeadY := 7;
    #NewX := 7;
    #NewY := 7;
    #FoodX := 12;
    #FoodY := 7;
    #SnakeLength := 3;
    #Score := 0;
    #GameOver := FALSE;
    #GameTick := FALSE;
    #Win := FALSE;
    #Seed := 123;
    #EatFood := FALSE;
    #SelfCollision := FALSE;
    #FoodOnSnake := FALSE;
    #NeedNewFood := FALSE;
    #FoodReady := FALSE;
    #BlinkVisible := TRUE;
    #BlinkTime := T#500ms;
    #BlinkCount := 0;
    
    FOR #i := 0 TO 255 DO
        #SnakeX[#i] := 0;
        #SnakeY[#i] := 0;
    END_FOR;
    
    #SnakeX[0] := 7;
    #SnakeY[0] := 7;
    #SnakeX[1] := 6;
    #SnakeY[1] := 7;
    #SnakeX[2] := 5;
    #SnakeY[2] := 7;

ELSE
    // Why:
    // Pause should stop game movement only.
    // It must not clear the snake, food, score, or display state.
    #GameTimer(IN := (#State = 20) AND NOT #Pause, PT := #MoveTime);
    #GameTick := #GameTimer.Q;
    
    // Why:
    // Seed is continuously changed so that the next food position is not always identical.
    #Seed := (#Seed + 1) MOD 32767;
    
    CASE #State OF
        0:
            // Why:
            // State 0 is a recovery state.
            // If an undefined state occurs, the FB returns to a known idle state.
            RESET_TIMER(#GameTimer);
            RESET_TIMER(#BlinkTimer);
            #Direction := 1;
            #NextDirection := 1;
            #HeadX := 7;
            #HeadY := 7;
            #NewX := 7;
            #NewY := 7;
            #FoodX := 12;
            #FoodY := 7;
            #SnakeLength := 3;
            #Score := 0;
            #GameOver := FALSE;
            #GameTick := FALSE;
            #Win := FALSE;
            #Seed := 123;
            #EatFood := FALSE;
            #SelfCollision := FALSE;
            #NeedNewFood := FALSE;
            #FoodReady := FALSE;
            #BlinkVisible := TRUE;
            #BlinkTime := T#500ms;
            #BlinkCount := 0;
            
            FOR #i := 0 TO 255 DO
                #SnakeX[#i] := 0;
                #SnakeY[#i] := 0;
            END_FOR;
            
            #SnakeX[0] := 7;
            #SnakeY[0] := 7;
            #SnakeX[1] := 6;
            #SnakeY[1] := 7;
            #SnakeX[2] := 5;
            #SnakeY[2] := 7;
            #State := 10;
            
        10:
            // Why:
            // Idle rebuilds initial game data.
            // This keeps Start as a clean command that always begins from the same predictable state.
            RESET_TIMER(#BlinkTimer);
            #GameOver := FALSE;
            #Win := FALSE;
            #Direction := 1;
            #NextDirection := 1;
            #HeadX := 7;
            #HeadY := 7;
            #NewX := 7;
            #NewY := 7;
            #FoodX := 12;
            #FoodY := 7;
            #SnakeLength := 3;
            #Score := 0;
            #EatFood := FALSE;
            #SelfCollision := FALSE;
            #NeedNewFood := FALSE;
            #FoodReady := FALSE;
            #BlinkVisible := TRUE;
            #BlinkTime := T#500ms;
            #BlinkCount := 0;
            
            FOR #i := 0 TO 255 DO
                #SnakeX[#i] := 0;
                #SnakeY[#i] := 0;
            END_FOR;
            
            #SnakeX[0] := 7;
            #SnakeY[0] := 7;
            #SnakeX[1] := 6;
            #SnakeY[1] := 7;
            #SnakeX[2] := 5;
            #SnakeY[2] := 7;
            
            IF #Start THEN
                RESET_TIMER(#GameTimer);
                #State := 20;
            END_IF;
            
        20:
            // Why:
            // Direction input is stored in NextDirection first.
            // The actual Direction changes only on GameTick, so the snake moves one grid per timing event.
            IF #BtnUp AND (#Direction <> 2) THEN
                #NextDirection := 0;
            ELSIF #BtnRight AND (#Direction <> 3) THEN
                #NextDirection := 1;
            ELSIF #BtnDown AND (#Direction <> 0) THEN
                #NextDirection := 2;
            ELSIF #BtnLeft AND (#Direction <> 1) THEN
                #NextDirection := 3;
            END_IF;
            
            IF #GameTick AND NOT #Pause THEN
                #Direction := #NextDirection;
                #NewX := #SnakeX[0];
                #NewY := #SnakeY[0];
                
                CASE #Direction OF
                    0:
                        #NewY := #SnakeY[0] - 1;
                    1:
                        #NewX := #SnakeX[0] + 1;
                    2:
                        #NewY := #SnakeY[0] + 1;
                    3:
                        #NewX := #SnakeX[0] - 1;
                    ELSE
                        #NewX := #SnakeX[0];
                        #NewY := #SnakeY[0];
                END_CASE;
                
                // Why:
                // Wall-through mode converts an out-of-range position into the opposite side.
                // Wall collision is intentionally removed, while self-collision is preserved.
                IF #NewX < 0 THEN
                    #NewX := 15;
                ELSIF #NewX > 15 THEN
                    #NewX := 0;
                END_IF;
                
                IF #NewY < 0 THEN
                    #NewY := 15;
                ELSIF #NewY > 15 THEN
                    #NewY := 0;
                END_IF;
                
                #EatFood := FALSE;
                #SelfCollision := FALSE;
                #NeedNewFood := FALSE;
                
                IF (#NewX = #FoodX) AND (#NewY = #FoodY) THEN
                    #EatFood := TRUE;
                END_IF;
                
                // Why:
                // When food is not eaten, the tail will move away, so the old tail position is allowed.
                // When food is eaten, the tail remains, so the full body must be checked.
                IF #EatFood THEN
                    #BodyLimit := #SnakeLength - 1;
                ELSE
                    #BodyLimit := #SnakeLength - 2;
                END_IF;
                
                IF #BodyLimit >= 0 THEN
                    FOR #i := 0 TO #BodyLimit DO
                        IF (#NewX = #SnakeX[#i]) AND (#NewY = #SnakeY[#i]) THEN
                            #SelfCollision := TRUE;
                        END_IF;
                    END_FOR;
                END_IF;
                
                IF #SelfCollision THEN
                    #GameOver := TRUE;
                    #State := 30;
                    RESET_TIMER(#GameTimer);
                    RESET_TIMER(#BlinkTimer);
                    #BlinkVisible := TRUE;
                ELSE
                    IF #EatFood THEN
                        IF #SnakeLength < 256 THEN
                            // Why:
                            // When food is eaten, the snake grows.
                            // The copy starts from SnakeLength, preserving the old tail and creating one new body segment.
                            FOR #i := #SnakeLength TO 1 BY -1 DO
                                #SnakeX[#i] := #SnakeX[#i - 1];
                                #SnakeY[#i] := #SnakeY[#i - 1];
                            END_FOR;
                            
                            #SnakeX[0] := #NewX;
                            #SnakeY[0] := #NewY;
                            #SnakeLength := #SnakeLength + 1;
                            #Score := #Score + 1;
                            #NeedNewFood := TRUE;
                        ELSE
                            #Win := TRUE;
                            #GameOver := TRUE;
                            #State := 40;
                            RESET_TIMER(#GameTimer);
                            RESET_TIMER(#BlinkTimer);
                            #BlinkVisible := TRUE;
                        END_IF;
                    ELSE
                        // Why:
                        // Normal movement does not change snake length.
                        // Each body segment copies the previous segment, then the new head is committed.
                        IF #SnakeLength > 1 THEN
                            #BodyLimit := #SnakeLength - 1;
                            FOR #i := #BodyLimit TO 1 BY -1 DO
                                #SnakeX[#i] := #SnakeX[#i - 1];
                                #SnakeY[#i] := #SnakeY[#i - 1];
                            END_FOR;
                        END_IF;
                        
                        #SnakeX[0] := #NewX;
                        #SnakeY[0] := #NewY;
                    END_IF;
                    
                    #HeadX := #SnakeX[0];
                    #HeadY := #SnakeY[0];
                    
                    IF #NeedNewFood THEN
                        // Why:
                        // Food must not appear on the snake body.
                        // The loop tries pseudo-random positions until it finds a free cell.
                        #FoodReady := FALSE;
                        #Seed := (#Seed + 17) MOD 32767;
                        
                        FOR #Attempt := 0 TO 255 DO
                            #FoodX := #Seed MOD 16;
                            #FoodY := (#Seed / 16) MOD 16;
                            #FoodOnSnake := FALSE;
                            #BodyLimit := #SnakeLength - 1;
                            
                            FOR #j := 0 TO #BodyLimit DO
                                IF (#FoodX = #SnakeX[#j]) AND (#FoodY = #SnakeY[#j]) THEN
                                    #FoodOnSnake := TRUE;
                                END_IF;
                            END_FOR;
                            
                            IF NOT #FoodOnSnake THEN
                                #FoodReady := TRUE;
                                EXIT;
                            ELSE
                                #Seed := (#Seed + 31) MOD 32767;
                            END_IF;
                        END_FOR;
                        
                        IF NOT #FoodReady THEN
                            #Win := TRUE;
                            #GameOver := TRUE;
                            #State := 40;
                            RESET_TIMER(#GameTimer);
                            RESET_TIMER(#BlinkTimer);
                            #BlinkVisible := TRUE;
                        END_IF;
                    END_IF;
                    
                    RESET_TIMER(#GameTimer);
                END_IF;
            END_IF;
            
        30:
            // Why:
            // GameOver is a stopped state.
            // Movement is disabled, and the display layer uses BlinkVisible to show a 1Hz warning effect.
            #GameOver := TRUE;
            RESET_TIMER(#GameTimer);
            
            IF #Start THEN
                #Direction := 1;
                #NextDirection := 1;
                #HeadX := 7;
                #HeadY := 7;
                #NewX := 7;
                #NewY := 7;
                #FoodX := 12;
                #FoodY := 7;
                #SnakeLength := 3;
                #Score := 0;
                #GameOver := FALSE;
                #Win := FALSE;
                #EatFood := FALSE;
                #SelfCollision := FALSE;
                #NeedNewFood := FALSE;
                #FoodReady := FALSE;
                RESET_TIMER(#BlinkTimer);
                #BlinkVisible := TRUE;
                #BlinkCount := 0;
                
                FOR #i := 0 TO 255 DO
                    #SnakeX[#i] := 0;
                    #SnakeY[#i] := 0;
                END_FOR;
                
                #SnakeX[0] := 7;
                #SnakeY[0] := 7;
                #SnakeX[1] := 6;
                #SnakeY[1] := 7;
                #SnakeX[2] := 5;
                #SnakeY[2] := 7;
                RESET_TIMER(#GameTimer);
                #State := 20;
            END_IF;
            
        40:
            // Why:
            // Win is separated from GameOver because its display meaning is different.
            // The display layer uses a faster 2Hz full-screen blink to indicate completion.
            #Win := TRUE;
            #GameOver := TRUE;
            RESET_TIMER(#GameTimer);
            
            IF #Start THEN
                #Direction := 1;
                #NextDirection := 1;
                #HeadX := 7;
                #HeadY := 7;
                #NewX := 7;
                #NewY := 7;
                #FoodX := 12;
                #FoodY := 7;
                #SnakeLength := 3;
                #Score := 0;
                #GameOver := FALSE;
                #Win := FALSE;
                #EatFood := FALSE;
                #SelfCollision := FALSE;
                #NeedNewFood := FALSE;
                #FoodReady := FALSE;
                RESET_TIMER(#BlinkTimer);
                #BlinkVisible := TRUE;
                #BlinkCount := 0;
                
                FOR #i := 0 TO 255 DO
                    #SnakeX[#i] := 0;
                    #SnakeY[#i] := 0;
                END_FOR;
                
                #SnakeX[0] := 7;
                #SnakeY[0] := 7;
                #SnakeX[1] := 6;
                #SnakeY[1] := 7;
                #SnakeX[2] := 5;
                #SnakeY[2] := 7;
                RESET_TIMER(#GameTimer);
                #State := 20;
            END_IF;
            
        ELSE
            #State := 0;
    END_CASE;
    
    // Why:
    // Blinking is handled separately from game movement.
    // This prevents GameOver or Win animation from changing snake data, food data, or score.
    // BlinkCount is added as a diagnostic counter because BlinkTimer.Q may be TRUE for only one PLC scan.
    IF #State = 30 THEN
        // Why:
        // GameOver uses 1Hz blinking.
        // A 1Hz full cycle is 1s, so the visible state toggles every 500ms.
        #BlinkTime := T#500ms;
        #BlinkTimer(IN := TRUE, PT := #BlinkTime);
        
        IF #BlinkTimer.Q THEN
            #BlinkVisible := NOT #BlinkVisible;
            
            IF #BlinkCount < 32767 THEN
                #BlinkCount := #BlinkCount + 1;
            ELSE
                #BlinkCount := 0;
            END_IF;
            
            RESET_TIMER(#BlinkTimer);
        END_IF;
        
    ELSIF #State = 40 THEN
        // Why:
        // Win uses 2Hz blinking.
        // A 2Hz full cycle is 500ms, so the visible state toggles every 250ms.
        #BlinkTime := T#250ms;
        #BlinkTimer(IN := TRUE, PT := #BlinkTime);
        
        IF #BlinkTimer.Q THEN
            #BlinkVisible := NOT #BlinkVisible;
            
            IF #BlinkCount < 32767 THEN
                #BlinkCount := #BlinkCount + 1;
            ELSE
                #BlinkCount := 0;
            END_IF;
            
            RESET_TIMER(#BlinkTimer);
        END_IF;
        
    ELSE
        // Why:
        // In normal game states, blinking should not affect display.
        // The display must remain visible and the blink timer must be reset.
        RESET_TIMER(#BlinkTimer);
        #BlinkVisible := TRUE;
    END_IF;
END_IF;

// Why:
// LedBuffer works as a display buffer.
// The game calculates all visual points in memory, then writes all rows to physical outputs at the end.
FOR #i := 0 TO 15 DO
    #LedBuffer[#i] := WORD#16#0000;
END_FOR;

IF NOT #Reset THEN
    IF #State = 40 THEN
        // Why:
        // Win animation uses full-screen blinking because it is visually clearer than showing the final snake only.
        IF #BlinkVisible THEN
            FOR #i := 0 TO 15 DO
                #LedBuffer[#i] := WORD#16#FFFF;
            END_FOR;
        END_IF;
    ELSE
        // Why:
        // During GameOver, the normal drawing logic is still used, but it is gated by BlinkVisible.
        // This produces a 1Hz blink without modifying snake or food data.
        IF (#State <> 30) OR #BlinkVisible THEN
            IF (#FoodX >= 0) AND (#FoodX <= 15) AND (#FoodY >= 0) AND (#FoodY <= 15) THEN
                IF #FoodX <= 7 THEN
                    #BitIndex := #FoodX + 8;
                ELSE
                    #BitIndex := #FoodX - 8;
                END_IF;
                
                #LedBuffer[#FoodY] := #LedBuffer[#FoodY] OR SHL(IN := WORD#16#0001, N := #BitIndex);
            END_IF;
            
            IF #SnakeLength > 0 THEN
                #BodyLimit := #SnakeLength - 1;
                
                FOR #i := 0 TO #BodyLimit DO
                    IF (#SnakeX[#i] >= 0) AND (#SnakeX[#i] <= 15) AND (#SnakeY[#i] >= 0) AND (#SnakeY[#i] <= 15) THEN
                        // Why:
                        // The internal coordinate X=0..15 is not the same as the Siemens WORD bit position.
                        // This mapping keeps game logic independent from the physical QW/QB/bit order.
                        IF #SnakeX[#i] <= 7 THEN
                            #BitIndex := #SnakeX[#i] + 8;
                        ELSE
                            #BitIndex := #SnakeX[#i] - 8;
                        END_IF;
                        
                        #LedBuffer[#SnakeY[#i]] := #LedBuffer[#SnakeY[#i]] OR SHL(IN := WORD#16#0001, N := #BitIndex);
                    END_IF;
                END_FOR;
            END_IF;
        END_IF;
    END_IF;
END_IF;

// Why:
// All physical LED outputs are assigned only once at the end.
// This avoids output conflict and makes Watch Table debugging easier.
#LedRow_00 := #LedBuffer[0];
#LedRow_01 := #LedBuffer[1];
#LedRow_02 := #LedBuffer[2];
#LedRow_03 := #LedBuffer[3];
#LedRow_04 := #LedBuffer[4];
#LedRow_05 := #LedBuffer[5];
#LedRow_06 := #LedBuffer[6];
#LedRow_07 := #LedBuffer[7];
#LedRow_08 := #LedBuffer[8];
#LedRow_09 := #LedBuffer[9];
#LedRow_10 := #LedBuffer[10];
#LedRow_11 := #LedBuffer[11];
#LedRow_12 := #LedBuffer[12];
#LedRow_13 := #LedBuffer[13];
#LedRow_14 := #LedBuffer[14];
#LedRow_15 := #LedBuffer[15];

END_FUNCTION_BLOCK //This is the end of the entire SCL code.

Hi Bob Chen:

Regarding your first response, it’s not necessary to go into so much detail to simply explain that hardwired logic is not the same as programmed logic. Your post essentially says the same thing as this post, only in more detail and demonstrating why students make the mistakes they do…

Regarding your second response, look, only you, the one who wrote the code, understand that code you’re showing. Nobody else understands it. Now, what you need to do isn’t show your code; what you need to do is create a video at least 15 minutes long where the effectiveness of that code can be verified. The snake should exit from one side and enter from the opposite side, both from left to right and from top to bottom. If I don’t see that in a video, I’m going to think your code doesn’t work, and if I don’t analyze it from the start, if I don’t see the video, I’m going to completely ignore it. Obviously, I’m not speaking for myself, but for a student in my shoes…

Now, when I finish contribution 127, I’m going to try to make the Snake game with Factory.io. I’ll try to do it in stages and upload it to my channel. The last video in the series will be the game running for at least 15 minutes. If this isn’t the case, I can’t say I’ve made the Snake game because it didn’t work. I’ll have learned, I’ll have tried, and whatever else you want to say, but if I’m not able to make a video of the Snake working, we can NEVER say the game is finished. In your case, it’s the same: if there aren’t videos of at least 15 minutes, then there’s no game. There will be other things, but they won’t be the Snake game working.

We’ll see how this turns out once I get started…

Regards

Hi, Amjavi6,

Following your request, I added the wall-crossing function. The snake can now pass through the top, bottom, left, and right boundaries.

I also added a pause function, so the game can be paused during operation. In addition, I added a counter function to clearly show the game progress.

If the snake hits its own body during the game, it will trigger Game Over, and the display will flash at 1 Hz.

After the 7-minute mark in the video, I demonstrate how to modify the movement speed. The default value is 300 ms. In the video, I changed it to 100 ms, but the game became too fast and ended almost immediately. When adjusted to 600 ms, the speed became too slow.

This shows that the speed is adjustable, and the programmer can decide how to modify it depending on the desired gameplay.

Please refer to my personal YouTube video:

Hi Bob Chen:

Now we can truly say you created the Snake game, congratulations!

I’m going to give it a try. To start, I’ll use 100 LEDs, 10 x 10 instead of 16 x 16, and see how it goes. We’ll see what happens…

We’ll demonstrate to the students that FactoryIO is the best simulator available today.

Regards

Hi Bob Chen:

Let’s see how the system behaves in the tests; it seems to be working well, now we’ll perfect it with the construction details:

Since I had the 100 bit program, I used it, but given the results, since it only works with the first and last digits of the list, it makes no difference whether it’s 10 x 10 or 16 x 16.

Regards

very very cool, congratulations !

Hello:

For this system to work, the food’s coordinates must be a multiple of 9; otherwise, it’s placed outside the line and activated if the snake passes through the adjacent row or column.

The bits that need to be controlled are the first one in the list, which should be turned off, and the last three, which should be turned on for safety. When the snake eats, two pieces are added at once, so the snake’s length doesn’t matter; only three bits are controlled.

In any case, the goal is to understand the game’s programming, not to create a perfect game. As you can see, a 10x10 board is sufficient.

Obviously, this can’t be done with hardwired logic; it must be done with programmed logic.

Regards

Hello:

Request to add a rating to each meal. I’m also taking this opportunity to make sure everything is working correctly after renaming the project. Here’s the result:

Regards