In my last submission: #9661: Katistrofic2's A2600 Escape from the Mindmaster "Level 1" in 00:13.02 I only completed the first of the 6 levels which Escape from the Mindmaster (1982) (Starpath) provides But this .BK2 beats the whole game in just over a minute.
I've already explained how this game functions on my previous submission so ill just quote what I said then:
Quoting Submission #9661: Katistrofic2's A2600 Escape from the Mindmaster "Level 1" in 00:13.02
In the Atari 2600 game: Escape from the Mindmaster from (1982) there are 6 levels of mazes of shapes, sometimes obstacles and enemies. The goal of all of these levels is to put all of the shapes into their corresponding holes and then reach the exit!
also this seems necessary for an understanding of how the RNG in the game works:
Quoting Submission #9661: Katistrofic2's A2600 Escape from the Mindmaster "Level 1" in 00:13.02
The positions of the shapes are dependent on RAM ADRESS 0x20 which is responsible for the RNG and there are 256 Different values for the RNG. Hence that means there are 256 different organizations for the positions of the shapes
to put all of the shapes into there "corresponding holes" as I've called them, I needed to use a script, last time I think I gave a pretty vague description of how this script works.
So this time ill explain it and try to give actual code lines to really make a good understanding for any publishers seeing this
SO what exactly should this script do? It needs to:
  • Enter a seed (RAM ADRESS 0x20)
    • Read through Mainmemory all the positions of the shapes and their holes
      • Calculate how long it would take (How many movements and rotations need to be done) to start at the beginning of a level, put all the shapes in and then get to the end
        • Print in the LUA Console output the result of the seed
          • Next seed
Now how did I do this? first we need to learn how we can "Enter a seed". The value of 0x20 on the frame the RESET button is pressed is the seed that you are inputing, so all I had to do was edit 0x20 with code and we can start at 0 and go up until 255:
local Rng_Byte=0x20
local currentseedcheck=0

while currentseedcheck<=255 do
	mainmemory.writebyte(Rng_Byte,currentseedcheck)
	emu.frameadvance();
	print(currentseedcheck)--------------------says the seed before telling me the least amount of movements for better formatting 
	print(findbestrouteforseed())---------------gives lowest amount of moves for this seed
	tastudio.setplayback(4596)-----------------In this case frame 4596 is the frame where RESET was pressed
	currentseedcheck=currentseedcheck+1
end
this code is very simple and it gives a very easy solution to this problem
now onto step 2 which is ALOT MORE COMPLEX. The way my code finds the best route for the seed goes as so: I have a function which can find the fastest route from 1 square to anotherill explain how this function works later using this function the script starts at the players starting location for the level to a shape, then that shapes hole then the next shape too find the best order of which shape to go to I do this:
local mapprioritypoints={{square,squarehole},{circle,circlehole},{M,Mhole},{X,Xhole}}

local allorders={{1,2,3,4},{1,2,4,3},{1,3,2,4},{1,3,4,2},{1,4,2,3},{1,4,3,2},
{2,1,3,4},{2,1,4,3},{2,3,1,4},{2,3,4,1},{2,4,1,3},{2,4,3,1},
{3,1,2,4},{3,1,4,2},{3,2,1,4},{3,2,4,1},{3,4,1,2},{3,4,2,1},
{4,1,2,3},{4,1,3,2},{4,2,1,3},{4,2,3,1},{4,3,1,2},{4,3,2,1}}

local function findbestrouteforseed()
	local best={["M"]=999,["R"]=999}    ------------------------"M" means movements and "R" rotations
	
	for _, order in ipairs(allorders) do-------------------------goes through every order
               for _,number in ipairs(order) do--------------goes through every number and the numbers represent different shapes
			for i=1,2 do --------------------goes to the shape and then its hole
				to={mainmemory.readbyte(mapprioritypoints[number][i])}
				addingfunction()
			end
		end
	end
end

this code bruteforses gets the movements for every order of shape collection. And with this code I can very easily pick the best one. here's the full code for the findbestrouteforseed() function just incase you don't understand how the rest is done:

local function findbestrouteforseed()
	local best={["M"]=999,["R"]=999}
	
	for _, order in ipairs(allorders) do

		local adding={["M"]=0,["R"]=0}
		local plrposition={7,"Down"}-----------------------the beginning square that needs to be manually changed based on the level
		local to={0}

		local function addingfunction(exact)-------------------this exact parameter determines if it should go directly to the block or to just face its direction, this is needed beacuse to get a shape you just need to be facing it not go to its exact location and I figured to make it an option for the end block as well
			local bestroutetoshape=pathfind(plrposition,to,exact)
			adding["M"]=adding["M"]+bestroutetoshape[1]["Moves"]
			adding["R"]=adding["R"]+bestroutetoshape[1]["Rots"]
			plrposition=bestroutetoshape[2]
		end
		
		for _,number in ipairs(order) do
			for i=1,2 do 
				to={mainmemory.readbyte(mapprioritypoints[number][i])}
				addingfunction()
			end
		end
		to={9,"Down"}------------exit that needs to be manually changed based on the level
		addingfunction(true)
		if adding["M"]<best["M"] then
			best=adding
			else
			if adding["R"]<best["R"] and adding["M"]==best["M"] then
				best=adding
			end
		end
	end
	return best
end


so now that thats explained I need to talk about how pathfind() works which gets the shortest amount of moves from 1 point to another so the function obviously needs to have the parameters of a block to start at then a block to end at so:
local pathfind(start,end,exact)-exact parameter determines if it should go directly to the block or to just face its direction, this is needed because to get a shape you just need to be facing it not go to its exact location
now how does this function actually do the task ahead of it?
  • It starts at its start block which was set in the parameters and moves forward until it finds a wall ahead of it.
    • For every block it moves it looks to the square at the left and right of it to see if theirs a possible turn it could make, If there is its added to its paths table.
      • after the end of the process of moving forward until it cant, it looks through the paths table and starts the process over but from those points
also for every step it eliminates the block behind it so the pathfinding system cant go back on its old path and make a infinite loop
lets show how i did this in code!
so first i used a table to define where all the walls are for the level and it looks like this:

-----1=free square
-----0=allsides blocked

-----2=left
-----3=right 
-----4=up
-----5=down
 
-----6=up and down 
-----7=left and right


----8=everything but up
----9=everything but down
----10=everything but left
----11=everything but right

----12=up and left
----13=up and right
----14=down and left
----15=down and right

 

local map={

1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,
1 ,8 ,9 ,9 ,9 ,1 ,0 ,1 ,
1 ,1 ,8 ,8 ,8 ,1 ,1 ,1 ,
1 ,9 ,9 ,9 ,9 ,1 ,13 ,1 ,
1 ,8 ,8 ,8 ,8 ,4 ,15 ,1 ,
1 ,1 ,9 ,9 ,9 ,1 ,1 ,1 ,
1 ,0 ,8 ,8 ,8 ,1 ,0 ,1 ,
1 ,1 ,1 ,1 ,3 ,1 ,1 ,1 ,


}

this was my map table for level 5. It can look pretty confusing at first glance but I put comments so that it can be easier.
1 means there are no walls around that square and as it says 10 would mean that there are walls in every direction except down and etc..
I also had to make a map for the doors that can be entered through 1 side but once you go through you cant go back. Basically the same concept though:

--------0=none

--------1=left
--------2=right
--------3=up
--------4=down

local one_way_walls_map={
0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,
0,2,2,2,4,0,0,0,
2,2,2,2,0,0,0,0,
0,0,0,0,2,0,0,0,
0,2,2,2,3,0,0,0,
0,0,0,0,0,0,0,0,
0,0,0,0,0,1,0,0,


}

--------------------------this code is also necessary because if all the indexes were integers you cant refence something like one_way_walls_map[2] and get 0 it would give back 2 because it thinks your talking about (find index with value 2) and not (get index 2) so i make it strings to prevent that. also it turns index 0 to index 1 and 1->2 and so on because thats how the game reads the grid map as well.

for i,v in ipairs(map) do
	if v==0 then map[i-1]="#" end
	if v==1 then map[i-1]=" " end
	if v==2 then map[i-1]="a" end
	if v==3 then map[i-1]="b" end
	if v==4 then map[i-1]="c" end
	if v==5 then map[i-1]="d" end
	if v==6 then map[i-1]="e" end
	if v==7 then map[i-1]="f" end
	if v==8 then map[i-1]="g" end
	if v==9 then map[i-1]="h" end
	if v==10 then map[i-1]="i" end
	if v==11 then map[i-1]="j" end
	if v==12 then map[i-1]="k" end
	if v==13 then map[i-1]="l" end
	if v==14 then map[i-1]="m" end
	if v==15 then map[i-1]="n" end
end
for i,v in ipairs(one_way_walls_map) do
	if v==0 then one_way_walls_map[i-1]=" " end
	if v==1 then one_way_walls_map[i-1]="a" end
	if v==2 then one_way_walls_map[i-1]="b" end
	if v==3 then one_way_walls_map[i-1]="c" end
	if v==4 then one_way_walls_map[i-1]="d" end
end
one_way_walls_map[64]=nil
map[64]=nil


so now that we have a representation of what the map is we need to define some other things to make this possible too
heres a list of those things:

----------------------------------------defines the added number for every direction
local directions={
	["Up"]=-8,
	["Down"]=8,
	["Left"]=-1,
	["Right"]=1,
}

	-------------------------------------------------makes walls work
	local walls={
	["#"]={["Left"]=true,["Right"]=true,["Up"]=true,["Down"]=true},

	[" "]={["Left"]=false,["Right"]=false,["Up"]=false,["Down"]=false},

	["a"]={["Left"]=true,["Right"]=false,["Up"]=false,["Down"]=false},

	["b"]={["Left"]=false,["Right"]=true,["Up"]=false,["Down"]=false},

	["c"]={["Left"]=false,["Right"]=false,["Up"]=true,["Down"]=false},

	["d"]={["Left"]=false,["Right"]=false,["Up"]=false,["Down"]=true},

	["e"]={["Left"]=false,["Right"]=false,["Up"]=true,["Down"]=true},

	["f"]={["Left"]=true,["Right"]=true,["Up"]=false,["Down"]=false},
	
	["g"]={["Left"]=true,["Right"]=true,["Up"]=false,["Down"]=true},

	["h"]={["Left"]=true,["Right"]=true,["Up"]=true,["Down"]=false},

	["i"]={["Left"]=false,["Right"]=true,["Up"]=true,["Down"]=true},
	
	["j"]={["Left"]=true,["Right"]=false,["Up"]=true,["Down"]=true},

	["k"]={["Left"]=true,["Right"]=false,["Up"]=true,["Down"]=false},

	["l"]={["Left"]=false,["Right"]=true,["Up"]=true,["Down"]=false},

	["m"]={["Left"]=true,["Right"]=false,["Up"]=false,["Down"]=true},

	["n"]={["Left"]=false,["Right"]=true,["Up"]=false,["Down"]=true},

	}

----------------------------left and right for every direction 
	local oposeingdirections={
	["Up"]={"Left","Right"},
	["Down"]={"Left","Right"},
	["Left"]={"Up","Down"},
	["Right"]={"Up","Down"},
	}

--------------------------opposite of every direction
	local oposeingdirections2={
	["Up"]="Down",
	["Down"]="Up",
	["Left"]="Right",
	["Right"]="Left",
	}


now I use a GoStright() function inside of the pathfind() function to make the part I said before. It needs to go straight until it finds a wall then look make at all the turns it could have made and do the same thing
now the GoStright() function has these parameters:
local function GoStright(startpos,eliminated_blocks,moves,Rotations,lookback)
startpos is self explanatory
eliminated_blocks is a table of all the blocks it already went through as I've said before to eliminate the inability of an infinite loop
moves and rotations is the amount of moves and rotations the player has executed from its starting point to where it is now
lookback is a bool. if its set to true it looks behind the player on the first block that was set for the gostraight function and if that block doesn't have a wall it puts it into paths table. its only the first block because every other time the check would find a wall due to the eliminated_blocks always putting a new wall behind after every move
now to significantly speed up the process of explaining the rest of the GoStright code ill just paste it here with comments that explains what everything does

	local paths={}

	
	local plrpos=startpos[1]-------------------------startpos is actually put in as a table to get the pos and rotation
	local plrRot=startpos[2]

	


	local one_way_walls={
	[" "]=nil,
	["a"]="Left",
	["b"]="Right",
	["c"]="Up",
	["d"]="Down",
	}




	local function testforwall(testrot)-------------------------this is the test for wall funtion
		
		---------------------------------this prevents any overlaping

		if testrot=="Left" then
			for i=0,7 do
				if plrpos==(8*i) then
					return true
				end
			end
		end
		if testrot=="Right" then
			for i=1,8 do
				if plrpos==(8*i)-1 then
					return true
				end
			end
		end

		---------------------------------------------------------


		-----------------------------------------one way wall collision 
		if one_way_walls[one_way_walls_map[plrpos]]==testrot then
			return false
		end
		--------------------------------------------------

		--------------------------------------------------tests if there is a wall in the direction specified
		if ((not map[plrpos]) or walls[map[plrpos]][testrot]) or ((not map[plrpos+directions[testrot]]) or walls[map[plrpos+directions[testrot]]][oposeingdirections2[testrot]]) then
	
			return true
		end
		----------------------------------------
	end


	local function lookforwinningblock()
		-----------------------------------unorganized goofy ahh code that looks in all directions for the block that was put as targetblock[2](targetblock[1] would be the desired rotation it would end up on)

		if exact then
			
			if targetblock[1]==plrpos then
				if targetblock[2] then
					if targetblock[2]==plrRot then
				return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
					end
					if targetblock[2]==oposeingdirections2[plrRot] then
				return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+2,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
					end
				return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+1,["plrpos"]=plrpos,["plrRot"]=targetblock[2], ["Found_end"] = true}
				end
				return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = true}
			end


			else
			

			for _,v in pairs(directions) do
			local v_string=reversetbl(directions)[tostring(v)]

				if plrpos+v==targetblock[1] and not testforwall(v_string) then
					if contains(oposeingdirections[plrRot],v_string) then
					return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+1,["plrpos"]=plrpos,["plrRot"]=v_string, ["Found_end"] = true}
						else
						if v_string==oposeingdirections2[plrRot] then
					return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations+2,["plrpos"]=plrpos,["plrRot"]=v_string, ["Found_end"] = true}
						end
					return {["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = true}
					end
				end
			end

		end
	end


	local function checkoposeingangles()-----------------------------function that looks left and right to see if there are no walls
		for _,v in ipairs(oposeingdirections[plrRot]) do
			if not testforwall(v) and not eliminated_blocks[plrpos+directions[tostring(v)]] then
				local newpathtable = {
  				  {plrpos + directions[v], v},
  				  moves + 1,
  				  Rotations + 1,
 				   copyTable(eliminated_blocks)
				}

				newpathtable[4][plrpos] = true  -- correctly eliminate the turn block
				table.insert(paths, newpathtable)
			end
		end
	end


	local counter=0
	
	while not testforwall(plrRot) and not eliminated_blocks[plrpos+directions[plrRot]] do-----------------loop
		local ifwon = lookforwinningblock()-------------------looks for winning block
		if ifwon then
			return ifwon
		end
		checkoposeingangles()

		---------------------------looks behind if its the first square
		if counter==0 and lookback then
			if not testforwall(oposeingdirections2[plrRot]) and not eliminated_blocks[plrpos+directions[tostring(oposeingdirections2[plrRot])]] then
				local newpathtable = {
  				  {plrpos + directions[oposeingdirections2[plrRot]], oposeingdirections2[plrRot]},
  				  moves + 1,
  				  Rotations + 1,
 				   copyTable(eliminated_blocks)
				}

				newpathtable[4][plrpos] = true  -- correctly eliminate the turn block
				table.insert(paths, newpathtable)
			end

		end

		counter=counter+1

-------------------------------------------------------------------------------------------------------for testing
-----------------if this is set to true it prints a grid of all the eliminated blocks. this shows the path that was done and was used for debuging
		if false then
			emu.yield();
			printGrid(eliminated_blocks,targetblock[1])
			emu.yield();
		end

------------------------------------------------------------------------------------------------------------------

		eliminated_blocks[plrpos]=true
		plrpos=plrpos+directions[plrRot]
		moves=moves+1
	end


	eliminated_blocks[plrpos]=true
	checkoposeingangles()
	local ifwon = lookforwinningblock()
	if ifwon then
		return ifwon
	end

	return {["eliminated_blocks"]=eliminated_blocks,["paths"]=paths,["moves"]=moves,["Rotations"]=Rotations,["plrpos"]=plrpos,["plrRot"]=plrRot, ["Found_end"] = false}------------------------returns all the data



then I use for loops inside of for loops to look through all the paths. then make paths for those paths, and paths for those paths.. so on
---------this sees if the end was found through the path it just finished
---------and if it was it checks to see if its better then the current best lowest number of movements
	local function checkforbest(info)
		if info["Found_end"]==true then
			if info["moves"] < best["Moves"] then
				best = {["Moves"] = info["moves"], ["Rots"] = info["Rotations"]}
				endposofbest={info["plrpos"],info["plrRot"]}
			else 
				if info["moves"] == best["Moves"] then
					if info["Rotations"] < best["Rots"] then
						best = {["Moves"] = info["moves"], ["Rots"] = info["Rotations"]}
						endposofbest={info["plrpos"],info["plrRot"]}
					end
				end
			end
		end
	end

	local info=GoStright(start,{},0,0,true)
	checkforbest(info)

	for _,path in ipairs(info["paths"]) do
		local info2=GoStright({path[1][1],path[1][2]},path[4],path[2],path[3])
		checkforbest(info2)

		for _,path2 in ipairs(info2["paths"]) do
			local info3=GoStright({path2[1][1],path2[1][2]},path2[4],path2[2],path2[3])
			checkforbest(info3)

			for _,path3 in ipairs(info3["paths"]) do
				local info4=GoStright({path3[1][1],path3[1][2]},path3[4],path3[2],path3[3])
				checkforbest(info4)

				for _,path4 in ipairs(info4["paths"]) do
					local info5=GoStright({path4[1][1],path4[1][2]},path4[4],path4[2],path4[3])
					checkforbest(info5)

					for _,path5 in ipairs(info5["paths"]) do
						local info6=GoStright({path5[1][1],path5[1][2]},path5[4],path5[2],path5[3])
						checkforbest(info6)

						for _,path6 in ipairs(info6["paths"]) do
							local info7=GoStright({path6[1][1],path6[1][2]},path6[4],path6[2],path6[3])
							checkforbest(info7)

							for _,path7 in ipairs(info7["paths"]) do
								local info8=GoStright({path7[1][1],path7[1][2]},path7[4],path7[2],path7[3])
								checkforbest(info8)

								for _,path8 in ipairs(info8["paths"]) do
									local info9=GoStright({path8[1][1],path8[1][2]},path8[4],path8[2],path8[3])
									checkforbest(info9)
-----------------------------------------this does mean that the script only can make a maximum of 9 turns but thats more then enough to get from 1 point to another
								end

							end

						end

					end

				end

			end

		end
	end


and i think that's just about everything in terms of explaining my script. if you would like to look at the full script go to this pastebin link : https://pastebin.com/1WAyExgP if you cant see the whole script through that link and you would like to just ask me for it, I because don't have like a GitHub or anything and I don't feel like making one
so first I would like to talk about the RNG manipulation: I've learned that every frame the RNG changes cording to some sort of pattern called a ("Linear Feedback Shift Register (LFSR) to generate pseudo-random numbers"). which is used by a lot of A2600 games. I learned this from Chat GBT. i just made a script that printed the RNG values through every frame, pasted it into chat gbt and it told me what it was. Now unfortunately I couldn't figure out exactly how the pattern works for this pacific Atari game. but that's not necessary. However what is important is the ways I found that it can be afflicted and manipulated:
you can actually speed up the RNG by pressing reset. This is why in my TAS to get the seed i want as fast as I can I spam reset. pressing reset puts the Rng ahead about 20-30 frames but this also means that if you press the reset button too close to the seed you want it will be jumped over. HOWEVER doing a forward movement advances the RNG by 1 frame. This is how I always got to just press reset like 2 or 3 times with perfect mashing and still get the best seed. also this is the reason why in my Tas I sometimes move backwards or forwards its either RNG manipulation or optimization and ill get into optimally moving right now
optimal movement and turns functionality: the player can move once every 16 frames and if he turns he needs to wait 4 frames before doing another movement. most of the time if he wants to turn twice he needs to turn and wait 8 frames until making the next turn. however if the direction he turns is left and then right he can in most cases do these both consecutively. But one exception to this could be:
if there was one of those squares that need to be passed with timing or the "obstacles" I refrenced at the beginning was recently on the screen. or if your unlucky ¯\_(ツ)_/¯
I think (could be wrong) that in very rare cases you can get a right then left in just 4 frames. But anyway this is why in most cases(whenever possible) I put in or obtain the shapes with a left and then right to get optimal turns.
anyway id like to talk about how to get through the "obstacles" ASAP. if you go into the obstacle without facing it (move backwards) you can get through it significantly faster then if you went through moving straight. I have 0 CLUE as to WHY? but I don't need to :)
also if you press left and right at same time the colors invert which is done for da memes
in the discussions page on my last submission I stated:
Quoting KATISTROFIC2
the seed you applied is actually the layout for the next 2 levels. Meaning that the best seed for level 1 isn't the best seed for 1 and 2 together and the same applies to 3 4 and 5 and 6.
this explains how the rng works. and how if you had the best seed for 1 level it might not be the best seed for the 2 levels combined
as for the seeds that were used. here they are:
level 1 & 2:
seed 157:
Movements:71, Rotations: 57

note (the next best seed is a whole 8 movement's higher meaning that My TAS is SURELY perfect from level 1 to 2)
level 3 & 4
seed 158:
M: 90 R: 64
coincidently the best seed I recorded for being the best for 3 and 4 is just 1 off the best for level 1 and 2. which is CRAZY! The next best seed is 4 movements off.
level 5:--level 6 isn't affected by RNG it's just a big maze with no shapes. making my job a lot easier. also there are 3 seeds which I think are basically the same for level 5 so i'll just list them all here. in the TAS I used seed 189 because it was the easiest accessible seed.
The 3 seeds:
177: "M": "77" "R": "42"
248: "M": "77" "R": "43"
189: "M": "77" "R": "43"
It's also worth saying that my script can't account for the "obstacles" and they actually make a HUGE difference in the time it takes to pass a seed. We actually lose around 2 seconds in the combined time lost through level 3 and 4 from obstacles and even more in level 5. So to make sure my seeds where optimal I used the second best seed for 3 and 4 and then was able to confirm that seed 158 for 3 and 4 is still the best. also I did the same thing for level 5. the top 3 seeds where all considered but where either worse or equal to using seed 189
The only possible way I think this TAS could be improved is if someone has a better understanding of the obstacles and how to get through them faster
I think that's just about everything that went behind this TAS's creation and everything I've learned about the game. I tried my best and put like 4 hours into writing this long submission to put as much detail as possible and I hope nobody will mind my lengthy submission.

Darkman425: Claiming for judging.
Darkman425: Removing the branch since it's not needed for this game.
The Lua script's effectiveness speaks for itself with the resulting input file looking optimized in its routing and movement. Good work!
Accepting to Standard.
fsvgm777: Processing. r3gamerz is handling the encodes for this one.


TASVideoAgent
They/Them
Moderator
Joined: 8/3/2004
Posts: 16239
Location: 127.0.0.1
This topic is for the purpose of discussing #9709: Katistrofic2's A2600 Escape from the Mindmaster in 01:58.10
Post subject: Movie published
TASVideoAgent
They/Them
Moderator
Joined: 8/3/2004
Posts: 16239
Location: 127.0.0.1
This movie has been published. The posts before this message apply to the submission, and posts after this message apply to the published movie. ---- [6578] A2600 Escape from the Mindmaster by Katistrofic2 in 01:58.10

1749156996