301 Days

A year of gamedev experiments.

Day 36 - Test 000001a

| Comments

Tests, Part 1 of NaN.

Coding

Ok, time to start moving forward on the whole “Let’s wrap this thing in tests so we can safely replace all of the tech” thing. QA terminology varies between industries, between groups in the same industry, between people in the same group even; but I like to call this kind of stuff “arm’s length” testing. Tests run, or can run, on a separate machine from the code being tested, and are run on a “release” build. It’s pretty natural for client-server systems, where we’ll just pretend to be a normal client (for the most part).

Let’s start with a Cucumber feature file:

features/login.featurelink
1
2
3
4
5
6
7
Feature: Login

Scenario: Logging in a new user
Given a new random user
And a telnet connection
When I create an account and character
Then I can play the game

And some definitions for those steps:

features/steps/user_steps.rblink
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Given(/^a new random user$/) do
  @user = {}
  @user[:id] = random_char_name(min_length = 4, max_length = 10)
  @user[:acct_name] = "test" + @user[:id].downcase
  @user[:acct_email] = "test" + @user[:id] + "@301days.com"
  @user[:acct_password] = "pass" + @user[:id]
  @user[:char_name] = "Dude." + @user[:id]
  @user[:char_gender] = ['1','2'].sample
  @user[:char_race] = ['I','M','L','LG','D','H','MN','B'].sample
  @user[:char_class] = ['FI','TH','WI','MA','TF'].sample
  puts "New random user: #{pp(@user)}"
end

Given(/^a telnet connection$/) do
  rand_id = rand(36**20).to_s(36)
  @connection = Net::Telnet::new("Host" => ENV['DS_HOST'],
              "Port" => 3000,
              "Output_log" => "output_#{rand_id}.log",
              "Dump_log" => "dump_#{rand_id}.log",
              "Prompt" => "Login:",
              "Telnetmode" => false,
              "Timeout" => 60,
              "Waittime" => 1)
end

When(/^I create an account and character$/) do
  @connection.cmd({"String" => "", "Match" => /Login\: /}) { |c| puts "0 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "new", "Match" => /account\: /}) { |c| puts "1 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_name], "Match" => /address\: /}) { |c| puts "2 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /address\: /}) { |c| puts "3 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /12\)\: /}) { |c| puts "4 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /password\: /}) { |c| puts "5 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /Gender\: /}) { |c| puts "6 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_gender], "Match" => /a Race\: /}) { |c| puts "7 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_race], "Match" => /Class\: /}) { |c| puts "8 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_class], "Match" => /\(y,n\)\: /}) { |c| puts "9 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "n", "Match" => /character\: /}) { |c| puts "10 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_name], "Match" => /Command\: /}) { |c| puts "11 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
end

Then(/^I can play the game$/) do
  @connection.cmd({"String" => "1", "Match" => / ->/}) { |c| puts "12 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
end

and we’re off to the races, once we define random_char_name.

features/steps/user_steps.rblink
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def random_char_name(min_length=4, max_length=14, prefix="Dude.")
  #public const short NAME_MIN_LENGTH = 4;
  #public const short NAME_MAX_LENGTH = 14;
  #string[] specialnames = new string[]{"orc","gnoll","goblin","skeleton","kobold","dragon","drake","griffin","manticore",
  #    "banshee","demon","bear","boar","vampire","ydnac","lars","sven","oskar","olaf",
  #    "marlis","neela","phong","ironbar","vulcan","sheriff","rolf","troll","wyvern",
  #    "ydmos","tanner","crazy.derf","trambuskar","ianta","alia","priest","statue",
  #    "shidosha","pazuzu","asmodeus","damballa","glamdrang","samael","perdurabo",
  #    "thamuz","knight","martialartist","thief","thaum","thaumaturge","wizard","fighter",
  #    "thisson"};
  #
  #string[] silly = new string[]{"pvp","lol","haha","hehe","btw","atm","jeje","rofl","roflmao","lmao","lmfao","lmho","dragonsspine",
  #    "dragonspine","nobody","somebody","anybody","account","character"};
  #
  #string acceptable = "abcdefghijklmnopqrstuvwxyz.";
  #
  specialnames = ["orc","gnoll","goblin","skeleton","kobold","dragon","drake","griffin","manticore",
                  "banshee","demon","bear","boar","vampire","ydnac","lars","sven","oskar","olaf",
                  "marlis","neela","phong","ironbar","vulcan","sheriff","rolf","troll","wyvern",
                  "ydmos","tanner","crazy.derf","trambuskar","ianta","alia","priest","statue",
                  "shidosha","pazuzu","asmodeus","damballa","glamdrang","samael","perdurabo",
                  "thamuz","knight","martialartist","thief","thaum","thaumaturge","wizard","fighter",
                  "thisson"]
  silly = ["pvp","lol","haha","hehe","btw","atm","jeje","rofl","roflmao","lmao","lmfao","lmho","dragonsspine",
      "dragonspine","nobody","somebody","anybody","account","character"]
  acceptable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".scan(/./)

To make sure I’m getting valid names, I started with code from the server’s CharGen.cs; specifically, CharacterNameDenied. But then why am I calling it with max_length of 10, and why no period in the acceptable array? Because I immediately ran into issues with the account name being rejected. A little digging found there were different rules there.

DragonsSpine/GameObjects/GameLiving/PC/Account.cslink
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public static bool AccountNameDenied(string name)
{
    // check if account exists
    if (Account.AccountExists(name))
        return true;

    // check account name length
    if (name.Length < Account.ACCOUNT_MIN_LENGTH || name.Length > Account.ACCOUNT_MAX_LENGTH)
        return true;

    bool deny = false;

    string[] silly = new string[]{"pvp","lol","haha","hehe","btw","atm","jeje","rofl","roflmao","lmao","lmfao","lmho","dragonsspine",
                                    "dragonspine","nobody","somebody","anybody","account"};

    string acceptable = "abcdefghijklmnopqrstuvwxyz0123456789";
DragonsSpine/GameObjects/GameLiving/PC/Account.cslink
9
10
11
12
    public const int ACCOUNT_MIN_LENGTH = 5;
    public const int ACCOUNT_MAX_LENGTH = 12;
    public const int PASSWORD_MIN_LENGTH = 4;
    public const int PASSWORD_MAX_LENGTH = 12;

So I’ll have to treat the various parts of the user a little differently. But for now, does this work?

cucumber.outlink
1
2
3
4
5
6
7
8
9
10
11
12
13
Feature: Login

{:id=>"CnQXY",
 :acct_name=>"testcnqxy",
 :acct_email=>"testCnQXY@301days.com",
 :acct_password=>"passCnQXY",
 :char_name=>"Dude.CnQXY",
 :char_gender=>"2",
 :char_race=>"L",
 :char_class=>"FI"}
  Scenario: Logging in a new user          # features/login.feature:3
    Given a new random user                # features/steps/user_steps.rb:4
      How about CnQXY?

cucumber.outlink
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
      Welcome testcnqxy!
      Current Character: Dude.CnQXY Level: 3 Class: Fighter Land: Beginner's Game Map: Island of Kesmai

      1. Enter Game
      2. Enter Conference Room
      3. Disconnect
      4. View Account
      5. Change Protocol (normal)
      6. Change/Create/Delete Character

      Command:
    Then I can play the game               # features/steps/user_steps.rb:44
      12 [ESC][2J[ESC][1;1f
      12 [ESC][2J[ESC][1;1f[ESC][3;6H. [ESC][3;8H. [ESC][3;10H. [ESC][3;12H. [ESC][3;14H. [ESC][3;16H| [ESC][3;18H  [ESC][4;6H. [ESC][4;8H. [ESC][4;10H. [ESC][4;12H. [ESC][4;14H. [ESC][4;16H[ESC][33m[][ESC][0m[ESC][4;18H[ESC][33m[][ESC][0m[ESC][5;6H[ESC][1;32m@@[ESC][0m[ESC][5;8Ho [ESC][5;10H. [ESC][5;12H. [ESC][5;14H. [ESC][5;16H. [ESC][5;18H. [ESC][6;6H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][6;8H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][6;10H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][6;12H::[ESC][6;14H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][6;16H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][6;18H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;6H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;8H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;10H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;12H::[ESC][7;14H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;16H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][7;18H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;6H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;8H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;10H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;12H::[ESC][8;14H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;16H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][8;18H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;6H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;8H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;10H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;12H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;14H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;16H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][9;18H[ESC][44m[ESC][1;37m~~[ESC][0m[ESC][2;24H A Sigur[ESC][3;10HA[ESC][3;24H*B 3 skeletons[ESC][5;6HB[ESC][6;12H^[ESC][22;1H                                                                                             [ESC][21;1H                                                                                             [ESC][20;1H                                                                                             [ESC][19;1H                                                                                             [ESC][18;1H                                                                                             [ESC][17;1H                                                                                             [ESC][16;1H                                                                                             [ESC][15;1H                                                                                             [ESC][14;1H                                                                                             [ESC][13;1H                                                                                             [ESC][12;1H                                                                                             [ESC][11;1H                                                                                             [ESC][10;1H                                                                                             [ESC][23;1H                             [ESC][23;1H [ESC][1;32mR flail[ESC][0m[ESC][24;1H                             [ESC][24;1H [ESC][1;32mL shield[ESC][0m[ESC][23;42H      [ESC][23;30H[ESC][35mHits       : 43/43    [ESC][0m[ESC][23;72H      [ESC][23;60H[ESC][35mHits Taken : 0[ESC][0m[ESC][25;42H     [ESC][25;30H[ESC][35mStamina    : 10[ESC][0m[ESC][24;42H        [ESC][24;30H[ESC][35mExperience : 1600  [ESC][0m[ESC][11;1H[ESC][21;1H ->

1 scenario (1 passed)
4 steps (4 passed)
0m21.221s

Hooray! Crude, but effective. A start.

A note about Cucumber

It looks like the online docs are being rewritten as we speak. I found saving a copy of the output of cucumber --help to be, well, helpful.


Playing

The Talos Principle

Second playthrough, about to try exploring the dialog trees now that I’ve done all of the main puzzles again.

Tearaway: Unfolded

Having played the original on the Vita, played through mostly to see the differences and to experience that world on the big screen. Even if you haven’t/won’t play it, go here for a glimpse of how it was built.


Day 36 code - tests

Comments