How do I create a multiplayer "text game" ?

  • Sorry that I didn't reply earlier.

    I don't get a notification when you edit your post without mentioning my username.

    Let's go through your questions one by one:

    From the first post:

    [quote:30npxdth]I see no way to ask for the "first" value in the meaning of "the first of the randomly generated string of value" but only in the meaning "lowest possible value (the opposite of end value)"

    [quote:30npxdth]asking for the "next" value for the first time will actually give the first value in the string

    Since Smart Random's Expression "next" starts at the minimum of the second value and only "start" returns the first value, you won't be able to simply always use "next".

    I suppose you use a function.

    You will call the function with the parameter of the random number index you want.

    (If you want the first random number, call the function with parameter 1, for the second, choose parameter 2 and so on)

    The function itself will check what the parameter ( Function.Param(0) ) is.

    If it is 1, it will set the function's return value to "SmartRandom.Start".

    If it is bigger than 1, it will set the function's return value to "SmartRandom.Next".

    Like this, you can simply call this function with the appropriate parameter when you need a new random number.

    _______________________________________

    [quote:30npxdth]Actually I have trouble now I am using families

    Try this .capx I created:

    Capx Download

    _________________________________________

    From the second post:

    [quote:30npxdth]I can't find a way to synchronize the "timer" global value. Everybody should have the host "timer", but I see no way to broadcast a number value.

    When the "timer" value is changed by the host, add an action "Multiplayer: Broadcast message".

    • From ID: Leave this like it is, since "" indicates that the message comes from the host, which is what we want
    • Tag: Change to what you like, for example "timer"
    • Message: Obviously, you put your global variable in this.
    • Mode: Leave this like it is too. This is only really important for real-time games (e.g. Multiplayer Shooter)

    Also, make sure that you add an event for the peers:

    "Multiplayer: On message "timer": Set timer to: 'Multiplayer.Message'".

    And make sure, no other action apart from this changes the global variable when you aren't the host.

    __________________________________

    [quote:30npxdth]I can't find a way to send to each peer, upon connection

    There are multiple ways to do this.

    First of all,

    add this event:

    "Multiplayer: On peer connected".

    This triggers when somebody joins the same room.

    Then, under that event, you can save the ID of the player that just connected or any other information you want by using "Multiplayer.From____" (e.g. FromID, FromAlias). This will return the information of the peer that just connected.

    Instead, you could also use "Multiplayer: PeerAliasAt(PeerCount-1)/PeerIDAt(PeerCount-1).

    This will return the same values since the player that just connected has to be the most recent player in the room and therefore, this will return his information. ("-1" since PeerAliasAt needs an index which starts at 0 and the PeerCount starts at 1)

    When you saved those values (to a dictionary, for example), you can send the needed information, e.g. the group number, to this peer using the aforementioned values.

    You need to add an action "Multiplayer: Send message" under the "On peer connected" event.

    As the peer ID, use the values mentioned above.

    The rest is the same as above.

    (Note: you don't have to save the ID/Alias, but it might be useful later)

    __________________________________________

    [quote:30npxdth]Is there a way to simplify the list of 30 events for spawning creatures using family ?

    See the .capx I posted above.

    _________________________________________________________-

    [quote:30npxdth]I feel like dictionnaries and arrays will come in handy here, but I am not yet familiar with those. Anyway, next step will be implementing the scoring system, so I will have to learn about it quite soon.

    Just tell me where you need help if the C2 manual didn't help you.

    Dictionary manual entry

    Array manual entry

    ________________________________________________________

  • Hey ! No problem, it's very cool that you keep answering and helping me <img src="{SMILIES_PATH}/icon_e_biggrin.gif" alt=":D" title="Very Happy">

    So, concerning the synchronization of global values, this is pretty much the same idea I had (found in this thread : https://www. scirra.com/forum/how-do-i-sync-global-variables-over-multiplayer_t101878). The problem is that the peer always gets a NaN value for the timer. Apparently, there is a problem with sending a variable as a message. The guy in the thread had the same problem and found an alternative solution which is not clearly explained, so I am stuck here.

    I will try the other stuff too and come back. (Just mentioning "randomly" in my edit will give you a notification ?)

    Edit randomly : Concerning the use of families. I looked at your .capx and I understand quite clearly how this work, but still, I can't think of a way to spawn my creatures according to my "Spawn" variable. As you can see in my .capx, I already have a "Creatures" family with all the sprites in it, and I have several events using this like : on start of layout > delete "Creatures" (and that's convenient). I also have the "index" instance variable set accordingly for each sprite, as you explained me. But I don't know how to say to the game "you have to spawn the creature who's "Index" value is egal to the current "Spawn" value". I tried your idea of "picking "Creature" by evaluating "Index = Spawn" but that did not work. That's the only detail that I am stuck with.

    • Concerning the smart random "next" expression. After some testing, it appears to be more simple than what you explained. "Start" expression always give you the lowest possible value in the range (useless in my case), but "next" actually starts from the first value of the sequence. Proof is in this .capx : https://www. dropbox.com/s/5l7ztd8kndlsov8/Next.expression-test.capx?dl=0

    Notice that the two first value you get are always different, which proves they belong to the same loop, thus the first time you click on "next" you really get the first value in the sequence. If you started at the second value, skipping the first one, then you would start at the edge of a loop and have a 50% chance that the first value of the next loop is the same.

    Illustration : with a sequence like 2-1 / 1-2 / 1-2 / 2-1 / ...

    The two first value you get using only "next" is this : 2-1 / 1-2 / 1-2 / 2-1 / ...

    And never : 2-1 / 1-2 / 1-2 / 2-1 / ...

    Which is what I want anyway. So that's cool. Now I have to think of the way to always send the "next" value to every peer upon connection. Of course, I tried "upon peer connected" > send message, tag "Group", message "Groupgenerator.Next". But that wouldn't work since "Groupgenerator.Next" is a number and not a string (damn, that limitation is troublesome).

    Here is another idea I just tried, but it failed (too bad! I was confident this time ah ah) : I created a "GlobalValues" dictionnary. The host task is to add a "Timer" key every second with the value "Timer" (global variable's current value), and add a key "Group" on peer connected, with the value "GroupGenerator.Next". Then, broadcast a message every second, tag "Timer", message "GlobalValues.Get(Timer)" and send a message on peer connected, ID "Multiplayer.FromID", tag "Group", message "GlobalValues.Get(Group)". For the peers : on peer message, tag "Timer"/"Group", set "Timer/Group" (Global variable) to "Multiplayer.message".

    And of course, it doesn't work for both : "Timer" gets a NaN value and "Group" doesn't change at all.

    • Concerning dictionnaries, I think I have a hard time understanding how the informations can be transmitted from host to peer. It seems the "AsJSON" expression is the key, but I don't really get it. For example, I suppose I could send the "GlobalVariables" Dictionnary as JSON and make the peer load it somehow and then set their global values accordingly. But I have troubles with this whole "host transforms into JSON > send to peers > Peers transform back to dictionnary objet" process. I think I would need a concrete example on that one.
  • [quote:1b7rhigt]Apparently, there is a problem with sending a variable as a message.

    Try wrapping a "str()" tag around the global variable.

    So that the message is 'str(variable)'.

    _____________________________________________-

    [quote:1b7rhigt]As you can see in my .capx, I already have a "Creatures" family with all the sprites in it

    Tried to look at your .capx, your file doesn't exist anymore under the link you posted.

    Please update the link.

    ____________________________________________

    [quote:1b7rhigt]I tried your idea of "picking "Creature" by evaluating "Index = Spawn" but that did not work

    This is exactly how it should work.

    Put the whole family in the picking event and set the condition as "Self.Index = [your_variable]".

    If that doesn't work, then it's the Spawn variable that is the problem.

    _____________________________________________

    [quote:1b7rhigt]But that wouldn't work since "Groupgenerator.Next" is a number and not a string (damn, that limitation is troublesome).

    I see, you are not familiar with "str()" and "int()" yet.

    You can remove number or string limitations by putting "str()" or "int()" around your variable.

    So when the expression needs to be a number, use "int(variable)".

    This will automatically take all the numbers that are in the string and make a number out of that.

    When you need a string, use "str(variable)".

    This will convert numbers or anything else to a string so you can e.g. send it via Multiplayer.

    Just remember to convert it back when receiving a message if needed.

    _________________________________________

    [quote:1b7rhigt]Here is another idea I just tried, but it failed (too bad! I was confident this time ah ah) : I created a "GlobalValues" dictionnary. The host task is to add a "Timer" key every second with the value "Timer" (global variable's current value), and add a key "Group" on peer connected, with the value "GroupGenerator.Next". Then, broadcast a message every second, tag "Timer", message "GlobalValues.Get(Timer)" and send a message on peer connected, ID "Multiplayer.FromID", tag "Group", message "GlobalValues.Get(Group)". For the peers : on peer message, tag "Timer"/"Group", set "Timer/Group" (Global variable) to "Multiplayer.message".

    May I ask what the "timer" variable is for?

    And what the exact purpose of the dictionary and the whole process around it is?

    I don't seem to grasp it yet.

    Broadcasting and using dictionary values shouldn't be too hard, so if you tell me what the purpose of it is, I can help you with it more easily.

    [quote:1b7rhigt]Then, broadcast a message every second, tag "Timer", message "GlobalValues.Get(Timer)"

    Also, why don't you just broadcast the variable directly?

    Why does it have to be in the dictionary first?

    ________________________________________________________

    [quote:1b7rhigt]But I have troubles with this whole "host transforms into JSON > send to peers > Peers transform back to dictionnary objet" process. I think I would need a concrete example on that one.

    As simple as this:

  • Thank you again for your answer.

    [quote:3mbk4f6l]Also, why don't you just broadcast the variable directly?

    Why does it have to be in the dictionary first?

    It was just an alternative solution I wanted to try because I already tried and failed with sending the variable directly. I also already tried with "int(variable)" but the peers always get 0 (the same problem happened in the thread I sent on my previous message). I will try with "str(variable)" this time, hopefully that will work.

    The "timer" is here so that everybody as X seconds to answer (probably 90 seconds for each creature). I want players to be synchronized on that and the timer to be displayed for them. The first round will be empty of creature, just so players have time to connect and synch with host and exchange a few messages with other players in their group. Then, every 90 seconds the content of "AnswerText" is sent to host and then comes the scoring system (not yet implemented).

    Concerning the dictionnaries, thank you for this simple example, this is what I thought but I wasn't sure. I did not have time to test a lot of stuff and everything failed so far, so I supposed I was doing something wrong.

    And here is the latest .capx : https://www. dropbox.com/s/i9d67roqu2jtql6/ChatProject.capx?dl=0

    On the debug, I can see the "Spawn" variable changine exactly as I want to, so I don't think that is the problem. But I may be wrong ^^

    Edit randomly : YEEAAAAAAHHHH !!! The str(variable) worked ! I can't believe I spent so much time on such a trivial thing ! But I guess that is what we call "learning" lol. Thank you ! Next : the scoring system. I will try something and send a new .capx.

    • Arf, actually it doesn't work with the "Group" value. I think I am doing something wrong with the "send message" action. I tried :

    For host : On peer connected >> send message, ID : Multiplayer.FromID, tag : "Group", message "str(GroupGenerator.Next)

    For peer : On peer message, tag "Group", set "Group" to "Multiplayer.Message"

    And since that did not work, I tried to first save "Multiplayer.FromID" in a global variable or in a dictionary and sending to that value instead, but it doesn't work either.

    "Timer" works fine because I use the "broadcast message" action instead of "send message".

  • [quote:3m3sgb9r]I want players to be synchronized on that and the timer to be displayed for them.

    I would just have the host broadcast one single message that will start a timer for the peers.

    Just use the "timer" behavior and start that when receiving the host's message.

    Of course, this will be client-dependant but less tedious than regularly sending a variable.

    You could also start a "timer" behavior for the host and use the "duration" expression minus the "currentTime" expression to broadcast the remaining time.

    That would make the dictionary obsolete.

    _________________________________

    [quote:3m3sgb9r]And here is the latest .capx : https://www. dropbox.com/s/i9d67roqu2jtql6/ChatProject.capx?dl=0

    Link doesn't work for me. 404.

    (Yes, I removed the blank space)

    ______________________________________

    [quote:3m3sgb9r]Arf, actually it doesn't work with the "Group" value. I think I am doing something wrong with the "send message" action.

    Actually, I think that this

    For peer : On peer message, tag "Group", set "Group" to "Multiplayer.Message"[/code:3m3sgb9r]
    is the issue.
    Try changing the "group" value to [i]int(Multiplayer.Message)[/i].
    See my previous post:
    
    This will convert numbers or anything else to a string so you can e.g. send it via Multiplayer.
    [b]Just remember to convert it back when receiving a message if needed.[/b]
    
    
    Btw, as a future tip:
    try to test whether messages are actually received with a simple debugging text object before changing the actions/methods.
    (Just add a text object and change it to "Multiplayer.Message" on Peer message to check whether the host or peer is the issue)
    
    ___________________________________
    
    [quote:3m3sgb9r]And since that did not work, I tried to first save "Multiplayer.FromID" in a global variable or in a dictionary and sending to that value instead, but it doesn't work either.
    
    Chuck that. Isn't really necessary since the expression already works as a global variable of some sort.
  • [quote:tpjbgdmw]I would just have the host broadcast one single message that will start a timer for the peers.

    But when would I send this message ? If someone joins after the message was sent, this peer won't have a timer. And for the scoring system I will need to be sure everyone send their answers upon host's signal (on the precise tick when timer reaches 0). Well anyway, this part is working for now. So that's not the most important.

    [quote:tpjbgdmw]try to test whether messages are actually received with a simple debugging text object before changing the actions/methods.

    I tried to do that after including "int(value)", but it stills doesn't work for the group value. The peer never gets their message upon connection, so there must be a problem on host side.

    Another try with the .capx: https://www. dropbox.com/s/palq7rp46p2k687/ChatProject.capx?dl=0

  • [quote:3fkvm5ld]But when would I send this message ? If someone joins after the message was sent, this peer won't have a timer.

    Earlier, you said that the timer is to give each player a timelimit on how long he can take to make a pick.

    I would generally say that you start the game no earlier than when all players joined a room (room is full).

    This will prevent you from needing to synchronize the timer every second or so.

    If a player disconnects during a game, I wouldn't let him join until the current round is over. That makes the whole process easier-

    _______________________________

    [quote:3fkvm5ld]I will need to be sure everyone send their answers upon host's signal

    Alright, that's quite a requirement.

    I would take the following approach:

    Since you are playing over LAN, latency shouldn't be much of an issue.

    This means that the timer's current times wouldn't be very different for each peer anyways.

    Do this:

    • When the round starts, the host gives each player the signal to start the timer
    • the timer for the peers is used to control the displayed timelimit only.

    That means that they will have visual feedback on their time left. (You probably wanted to do that anyways)

    But when the peer's timer reaches 0, they won't send their message yet.

    They will wait until the host broadcasts a message, that his timer is over.

    This will probably cause a slight difference delay the peer's visual timer reaching 0 and the host's message arriving, but this will guarantee that each player sends his message at the same time.

    Long story short:

    • The peer's timer is started at the same time as the host's timer
    • The peer's timer is only used for displaying the time left, not to send the message when the timer reaches 0
    • When the host's timer reaches 0, he broadcasts a message (e.g. "timer"). When the peers receive this message, they send their answer (synchronized)

    __________________________________

    [quote:3fkvm5ld]I tried to do that after including "int(value)", but it stills doesn't work for the group value. The peer never gets their message upon connection, so there must be a problem on host side.

    Alright.

    Try to set the "Send message" Peer ID to Multiplayer.PeerIDAt(Multiplayer.PeerCount-1).

    (I'll explain that if it actually works)

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • [quote:2z500p6m]Try to set the "Send message" Peer ID to Multiplayer.PeerIDAt(Multiplayer.PeerCount-1).

    (I'll explain that if it actually works)

    It does work . So what is the difference between "Multiplayer.PeerIDAt(Multiplayer.PeerCount-1)" and "Multiplayer.FromID" ?

  • Apparently, quite a big one.

    No, I was just wrong when I thought that FromID returns the ID of the recently connected peer when put under a "On peer connected" event.

    Cause, you know, that would make sense.

    But apparently, that's not the case, so I tried something else (PeerIDAt) and that worked.

    Now, to explain Multiplayer.PeerIDAt:

    it takes the index of a player and returns his ID.

    The index is different for each player.

    So, when there were two players in a room, me and you and I used PeerIDAt(0), it would return my ID while PeerIDAt(1) would return yours.

    The other way around, if you used the expression, (0) would be your ID and (1) would be mine.

    When a player joins though, he will always be added on the last position of this table.

    That's why we use PeerCount.

    We substract 1 because PeerCount starts as 1 and the PeerIDAt expression needs an index, which starts at 0.

  • Oh ok I get it. (Well it's still weird that ".FromID" doesn't work though).

    Well I actually managed to implement the scoring system too and changed the way the "timer" works to what you explained earlier. So now I could say the game is "fully operational". Here is the .capx : https://www. dropbox.com/s/gv08nlsdwl1xbcx/ChatProject.capx?dl=0

    There is still place for improvement though, here is how it works now :

    • When the peercount = "the number of player I want", host broadcasts his timer, then everybody substract 1 every second (it's only visual for peers).
    • When host's timer reaches 0, he broadcasts "timer0" to everyone. He also registers his answer (if not empty) on a dictionary with value 1. And puts "timer" back to 10 seconds.
    • When peers get "timer0", they send their answers to host (if not empty, tag "Answer"). They put their "timer" to 10 seconds too.
    • When host gets "Answer", he adds this to the dictionary (either a new key with value 1 or +1 to an already existing key).
    • When host's "timer" is 9, he checks if the dictionary has a key corresponding to his "Answer.Text". If so, add key value -1 to your score (you don't get a point for yourself). He then broadcasts the dictionary as JSON and peers do the same.

    Note that everybody saves their own answers only with the "Spawn" value for later analyzes purposes. But this has nothing to do with the scoring system.

    My problem here is the last step has to happen at host's timer = 9, which is already 1 second in the next round (so there is a delay between the end of the round and the actual scoring). But when trying with timer=10, it seemed that the host did not have time to get the peers' "Answer" messages. Which is weird, since this should happen at timer = 0, thus, one frame earlier than timer = 10. Also, this would be a problem for scoring the last round (well, it's not really hard to solve that). Do you think this is actually just because it takes time to exchange all those messages ? Do you have a better idea to improve that ? I thought about asking the host to count the answers he gets and set timer to 10 only when that number reaches peercount-1. But then I should allow peers to send empty answers and find another way to exclude those from the dictionary, so nobody get points for not answering.

  • Sorry for the late answer, I was on Christmas holidays.

    ________________________________

    [quote:24fpnv3a]When peers get "timer0", they send their answers to host (if not empty, tag "Answer")

    Don't know whether you added this yet or not, but it may be sensible to catch the case that every player submits an empty answer, by accident.

    So that it doesn't display "", but "No answer given" for example.

    ________________________________________

    [quote:24fpnv3a]I thought about asking the host to count the answers he gets and set timer to 10 only when that number reaches peercount-1.

    Yep, that is exactly what I would do.

    Don't start a new round before the host didn't receive all answers.

    You could also add a timeout, so that when the timer is 0, he waits 30 seconds and after that, he uses the answers he received no matter whether there are some missing (which should be unlikely anyways).

    ________________________________________________

    [quote:24fpnv3a] find another way to exclude those from the dictionary, so nobody get points for not answering.

    Just compare the content of the received message.

    If "Multiplayer.Message" equals "", then it is empty.

    If you invert this condition, it isn't.

    Easy as that.

    _____________________________________

    I'm happy you are making progress.

  • Hey ! No problem, I was on holydays too.

    Actually, I will be quite busy with other stuffs until the middle of february, so I may have to put the game aside for a while. But I think it is already in good shape (thanks to your good advices).

    The next thing I will have to think about is this : the "previous creatures" hint.

    I want it to be a page that appears when "previous creatures" button is clicked. It would show the previous creatures sprite (in reduced size) + the answer given by the player and the points he got for that round. I am sure there is a quite simple way to do this. I just haven't had time to think about it yet. If you have any advice...

  • [quote:2ut3bivj]I want it to be a page that appears when "previous creatures" button is clicked. It would show the previous creatures sprite (in reduced size) + the answer given by the player and the points he got for that round. I am sure there is a quite simple way to do this. I just haven't had time to think about it yet. If you have any advice...

    In my opinion, the hardest thing while doing this, is to find a proper way to layout this to not make it obliterate the whole program haha.

    But that's up to you.

    Well, again, I'll have to refer to Dictionaries for this case (they are so useful).

    Since every peer receives the current creature anyways, you can simply submit that creature to a "creature"-dictionary when receiving the creature for the current round. Choose the current round's number as the key.

    (Don't forget to add "str()" around the number, since dictionaries only take strings as keys)

    (And to use "int()" when parsing the dictionary's contents)

    The same goes for the answer and the points. When you submit your answer/get your points, add the answer/points to a "myAnswers"/"myPoints" dictionary with the current round as a key (-> "str()").

    Now when a new round starts (and it isn't the first round), get the creature, answer and points from the previous round using a "For each key" condition (from the dictionary).

    Under that condition, you can retrieve the current dictionary's contents with the expression "[Dictionary].currentValue".

    Use these steps for the three dictionaries (creature, answer, points) and voilà, you have your previous answers at your fingertip(s).

  • Yeap, dictionnaries for life lol. My first idea was to use families though. Something like creating 30 sets of 3 items (at the beginning, only "?" would appear) : a sprite, a "answer" text and a "score" text. Put the 3 items in 3 different families with an instance variable (corresponding to the creature count). When timer reaches 0 :

    Pick sprite (currently a question mark picture) by instance variable = creature count => Replace with sprite of current creature

    Pick "answer" text (currently a question mark) by instance variable = creature count => Replace it with Answer.Text

    Pick "score" text (currently a question mark) by instance variable = creature count => Replace it with "HostAnswerList.get(Answer.Text)-1"

    Do you think I can do something along those lines ?

    [quote:1hkiznfi]Don't know whether you added this yet or not, but it may be sensible to catch the case that every player submits an empty answer, by accident.

    So that it doesn't display "", but "No answer given" for example.

    I don't understand what would be the problem if everybody submit an empty answer. So far, empty answer = you don't send anything to host and you don't score anyway when getting the answer list.

    Also, I notice a little problem with the way the scoring system works now : if someone always answer the same thing, like "ZZZZZ" for example, even if it is nonsense, it will add 1 to "ZZZZZ" value every round. So this player would score round number-1 every round. I should find a way to prevent previous rounds to influence later ones. I thought about 2 solutions :

    • Reset HostAnswerList every round => cool, but a complete list of all answers could have been interesting data to analyze for my research, so I'd rather not.
    • Learning to use array, then I can have x = Answer (text), y = Value for this answer, z = round number. And players would score y points only if there Answer.Text = x and round number = z. => I have to learn how to use array T_T
  • [quote:1qfanoxy]Pick sprite (currently a question mark picture) by instance variable = creature count => Replace with sprite of current creature

    Pick "answer" text (currently a question mark) by instance variable = creature count => Replace it with Answer.Text

    Pick "score" text (currently a question mark) by instance variable = creature count => Replace it with "HostAnswerList.get(Answer.Text)-1"

    [quote:1qfanoxy]Do you think I can do something along those lines ?

    Yup, that should work too. (I'm just too much of a fan of Dictionaries to not propose them).

    _____________________________________

    [quote:1qfanoxy]So far, empty answer = you don't send anything to host

    Ah ok, if you included that already, then you won't have to worry about it.

    ___________________________________

    [quote:1qfanoxy]Also, I notice a little problem with the way the scoring system works now : if someone always answer the same thing, like "ZZZZZ" for example, even if it is nonsense, it will add 1 to "ZZZZZ" value every round. So this player would score round number-1 every round. I should find a way to prevent previous rounds to influence later ones.

    Well, we both know that we need a way to differentiate between answers given in different rounds.

    The way I'd probably go (because I find 3d-Arrays too tedious to use, though they are very powerful), is to include the "round" number in the actual answer.

    So for example, the dictionary-entry for the first answer could look like this:

    · Key: "1" / · Value: "1//[answer]"

    The second answer would look like this:

    · Key: "2" / · Value: "2//[answer]"

    Now you can see which answer comes from which round.

    Of course, you will need to extract the plain answer from the key.

    If you know, that the number of rounds will never] go above 9, you simply use "left(3,length([string]))".

    Otherwise, you will have to use regex. (RegexMatchAt([string],"\d\/{2}([\d\D]+)","",0)

    __

    Or, you can learn 3d-arrays. :D

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)