Social Networks in Games: Playing with Your Facebook Friends

This is another article in Game Programming Gems 8, talking about accessing web services of social networks from our own games. For example, log people in using their Facebook account.

RESTful Web Service

Representational State Transfer (REST) is the predominant architecture for offering programmatic access to data stored on the web. A RESTful service is composed of a collection of resources, which are identified by a web address, such as http://example.com/resource. It is based on stateless operations, which means any state information is held in the client, so a service can scale to a large number of clients–ideal for web services.

In practice, a RESTful service works with HTTP requests. We send HTTP GET to retrieve data, and the response are usually in JavaScript Object Notation (JSON) format, which looks like this:

{“trends”:{“2009-08-23 04:00:47”:[
  {“query”:”\”Best love song?\”“,”name”:”Best love song?”},
  {“query”:”#fact”,”name”:”#fact”},
  {“query”:”#shoutout”,”name”:”#shoutout”},
  {“query”:”#HappyBDayHowieD”,”name”:”#HappyBDayHowieD”},
  {“query”:”\”District 9\”“,”name”:”District 9”},
  {“query”:”\”Inglourious Basterds\”“,”name”:”Inglourious Basterds”},
  {“query”:”\”Hurricane Bill\”“,”name”:”Hurricane Bill”},
  {“query”:”#peacebetweenjbfans”,”name”:”#peacebetweenjbfans”},
  {“query”:”#Nascar”,”name”:”#Nascar”},
  {“query”:”Raiders”,”name”:”Raiders”}
]},”as_of”:1251000047}

Authenticating a User

Normally we have to confirm a user’s identity before we gain access to data. The most basic authentication mechanism requires users to enter a user name and password, which our application sends to the web service. It requires users to trust our application not to collect passwords and abuse them for other purposes. This fear might stop users from trying out new applications.

Applications on the web have answered this need by offering authentication mechanisms based on forwarding. When logging into an application, users will be forwarded to the login page of the account provider and enter user name and password there. The application will never see user’s credentials, but will only receive a confirmation of whether the login was successful.

Facebook Login

Let’s try the Facebook Login on a web page. There are 4 steps:

  1. Set Redirect URLs.
  2. Check the login statues.
  3. Log people in.
  4. Log people out.

The first step can be done in Facebook App Settings. It ensures Facebook login page only responses to calls from valid URLs.

When loading our webpage, the first thing to do is check if a user is already logged into our application with Facebook Login. We can start that process with a call to FB.getLoginStatus, this function will trigger a call to Facebook to get the login status and call our callback function with the results.

However definitely we should load and initialize Facebook JavaScript SDK. Here is the codes:

<script>
  // This Init function should be inserted
  // directly after the opening  tag
  window.fbAsyncInit = function() {
    FB.init({
      appId            : 'your-app-id',
      cookie           : true,  // enable cookies to allow the server to access
                                // the session
      autoLogAppEvents : true,
      xfbml            : true,  // parse social plugins on this page
      version          : 'v2.12'
    });
  };

  // Load the SDK asynchronously
  (function(d, s, id){
     var js, fjs = d.getElementsByTagName(s)[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement(s); js.id = id;
     js.src = "https://connect.facebook.net/en_US/sdk.js";
     fjs.parentNode.insertBefore(js, fjs);
   }(document, 'script', 'facebook-jssdk'));
</script>

Now that we’ve initialized the JavaScript SDK, we call FB.getLoginStatus(). This function gets the state of the person visiting this page and can return one of three following states:

  • Logged into our app (‘connected’)
  • Logged into Facebook, but not our app yet (‘not_authorized’)
  • Not logged into Facebook, so we cannot tell if they are logged into our app or not (‘unknown’)

These three cases are handled in the callback funtion.

<script>
  // This Init function should be inserted
  // directly after the opening &amp;amp;lt;body&amp;amp;gt; tag
  window.fbAsyncInit = function() {
    FB.init({
      appId            : 'your-app-id',
      cookie           : true,  // enable cookies to allow the server to access
                                // the session
      autoLogAppEvents : true,
      xfbml            : true,  // parse social plugins on this page
      version          : 'v2.12'
    });

    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });

  };

  function statusChangeCallback(response) {
    console.log('statusChangeCallback');
    console.log(response.status);  // three states
  }

  // Load the SDK asynchronously
  (function(d, s, id){
     var js, fjs = d.getElementsByTagName(s)[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement(s); js.id = id;
     js.src = "https://connect.facebook.net/en_US/sdk.js";
     fjs.parentNode.insertBefore(js, fjs);
   }(document, 'script', 'facebook-jssdk'));
</script>

Once our app knows the login status of the person using it, it can do one of the following:

  • If the person is logged into Facebook and our app, redirect them to our app’s logged in experience.
  • I the person isn’t logged into our app or isn’t logged into Facebook, prompt them with the Login dialog with FB.login() or show them the Login Button.

Facebook provides an easy way to generate a Login Button by one click:

WX20180214-004201

After user logged in, we can access authorized data using FB.api(). I have made a demo on http://chenglongyi.com/test/fbapi/, and the full code is below:

<!DOCTYPE html>
<html>
<head>
  <title>Facebook login</title>
</head>
<body>

<script>
  window.fbAsyncInit = function() {
    FB.init({
      appId      : 'your-app-id',
      cookie     : true,  // enable cookies to allow the server to access 
                          // the session
      xfbml      : true,  // parse social plugins on this page
      version    : 'v2.12' // use graph api version 2.8
    });

    // Now that we've initialized the JavaScript SDK, we call 
    // FB.getLoginStatus().  This function gets the state of the
    // person visiting this page and can return one of three states to
    // the callback you provide.  They can be:
    //
    // 1. Logged into your app ('connected')
    // 2. Logged into Facebook, but not your app ('not_authorized')
    // 3. Not logged into Facebook and can't tell if they are logged into
    //    your app or not.
    //
    // These three cases are handled in the callback function.

    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });
  };

  function checkLoginState() {
    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });
  }

  function statusChangeCallback(response) {
    console.log('statusChangeCallback');
    console.log(response);

    if (response.status === "connected") {
      welcome();
    } else {
      document.getElementById('welcome').innerHTML = "";
    }
  }

  function welcome() {
    FB.api(
      '/me',
      'GET',
      {"fields":"id,name"},
      function(response) {
        console.log(response);
        document.getElementById('welcome').innerHTML = "Welcome " + response.name + "!";
      }
    );
  }

  // Load the SDK asynchronously
  (function(d, s, id) {
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) return;
    js = d.createElement(s); js.id = id;
    js.src = "https://connect.facebook.net/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

</script>
<div class="fb-login-button" data-max-rows="1" data-size="large" data-button-type="continue_with" data-show-faces="false" data-auto-logout-link="true" data-use-continue-as="true" onlogin="checkLoginState"></div>

</body>
</html>
Advertisements

Behavioral Questions

Following Dave’s advice, I am reading the book CRACKING THE CODING  INTERVIEW. It is a great book, not only listed all the knowledge I should know to pass the coding tests, but also mentioned how to prepare for general non-tech questions I may neglect, such as behavioral questions. Here are some tips.

Projects

Questions often come from the projects listed on resume. So to ensure I can talk more details about them, those projects should be selected following these criteria:

  • The project had challenging components (beyond just “learning a lot”).
  • I played a central role (ideally on the challenging components).
  • I can talk at technical depth.

Here are more components would be helpful for going through each project. This grid can be filled with some keywords, and put it in front of me during an interview as a reminder.

Common Questions Project 1 Project 2 Project 3
Challenges
Mistakes/Failures
Enjoyed
Leadership
Conflicts
What You’d do Differently

Response

When answering behavioral questions, it should be specific, but with limited details, and offers an opportunity for the interviewer to drill in further. For example, putting “I can go into more details if you’d like” after a clear and short answer.

The expanded answer should be structured. Start with a “nugget” succinctly describes what it will be about, then approach it via three steps of Situation, Action and Result. The Action should be more detailed as it is the most important step, so break it into multiple parts to encourage sufficient depth. Also, rephrase it in a better way to demonstrate personal attributes like Initiative, Leadership, Empathy, Compassion, Humility, Teamwork and Helpfulness.

Here is also a grid would be helpful for organizing stories:

Nugget Situation Actions Result Attributes
Story 1 1…2…3…
Story 2 1…2…3…

Weaknesses

To avoid making myself looks arrogant, give a real weakness. I think my biggest weakness now is time management and execution.

Fast-IsA

This is a GEM from Game Programming Gems 8, by Joshua Grass, provides a better method for processing class hierarchy data that can increase the efficiency of IsA check from O(N) to O(1). Here is my summary after reading.

Given a typical class hierarchy like below:

WeChat Image_20180130164516

A normal way to perform IsA check, determine whether Class A is a subclass of Class B would be:

bool IsA(Class *pA, Class *pB)
{
    while (*pA != NULL)
    {
        if (pA == pB)
        {
            return true;
        }
        pA = pA -> GetParentClass();
    }
    return false;
}

The worst case of this algorithm would perform a traversal from leaf to root, which can be very expensive.

So let’s simplify the problem. Imagine we are lucky and the class is in a perfectly balanced binary hierarchy, which means each node branched exactly twice, just like below:

WeChat Image_20180130174959

In this situation the class hierarchy can be put into a contiguous array.

WeChat Screenshot_20180130175525

We noticed that on each level we add 2^(N-1) new nodes to the array, where N is the new level. According to the index in the array(the second row in above table), we can easily find the relationship between each node and its parent:

int parentIndex(int nodeIndex)
{
    return nodeIndex >> 1;
}

So according to this algorithm, on a perfectly balanced tree, we can improve IsA() from O(N) to O(logN):

bool IsA_Balanced2Tree(Class *pA, Class *pB)
{
    int nodeAIndex = pA -> GetClassIndex();
    int nodeBIndex = pB -> GetClassIndex();

    while (nodeAIndex != 0)
    {
        if (nodeAIndex == nodeBIndex)
        {
            return true;
        }
        nodeAIndex = nodeAIndex >> 1;
    }
    return false;
}

Actually once the index for a parent of A is less than the index for B, there is no way that they can be equal. So we can reduce worst-case scenario.

bool IsA_Balanced2Tree_V2(Class *pA, Class *pB)
{
    int nodeAIndex = pA -> GetClassIndex();
    int nodeBIndex = pB -> GetClassIndex();

    while (nodeAIndex >= nodeBIndex)
    {
        if (nodeAIndex == nodeBIndex)
        {
            return true;
        }
        nodeAIndex = nodeAIndex >> 1;
    }
    return false;
}

According to the relationship between a node and its parent, here is the algorithm to find the index of a child, depends on its position in the sub-tree:

int childIndex(int nodeIndex, bool bRight)
{
    if (bRight)
    {
        return (nodeIndex << 1) + 1;
    }
    else
    {
        return (nodeIndex << 1);
    }
}

The binary representation looks like this:

WeChat Image_20180130190605

We can observe that: if Class A is a child of Class B, then the leftmost N bits of B will match A, where N is the highest bit set in A.

Using this rule we can remove the while loop in IsA():

bool IsA_Balanced2Tree_V3(Class *pA, Class *pB)
{
    int nodeAIndex = pA -> GetClassIndex();
    int nodeBIndex = pB -> GetClassIndex();

    if (nodeAIndex > (BSR(nodeAIndex) - BSR(nodeBIndex));

    return nodeAIndex == nodeBIndex;
}

The BSR() here is a wrapper for an inline assembly function that uses the BSR assembly instruction BitScanReverse, which returns the index of the leftmost set bit. If would be easy to pre-calculate the most significant bit index even if the platform does not support BSR().

However, all of the previous work has been built upon the notion that our class hierarchy is a perfectly balanced tree. Fortunately, our IsA() function does not care about the depth between nodes, only ancestry matters. So we can convert the unbalanced tree into balanced tree by adding phantom class, as long as we keep them in fact ancestors.

WechatIMG144

The following algorithm is the simplest implementation for building the class tree.

void BuildTree(Class *pA)
{
    int nCurrentClassIndex = pA -> GetClassIndex();
    int nNumChildClasses = pA -> GetNumberOfChildClasses();
    int nNumLeverls = BSR(nNumChildClasses() + 1;
    int nChildIndexStart = nCurrentClassIndex << nNumLevels;

    for (int i = 0; i  GetChildClass(i);
        pChild -> SetClassIndex(nChildIndexStart + i);
        BuildTree(pChild);
    }
}

Independent Study for Spring 2018

I am taking the Independent Study for Spring 2018, my last semester in ETC, focusing on strengthening my programming skills, and keeping practice on solving technical interview questions, to make myself well-prepared for finding a job.

More specifically,  here are three main goals:

  • Get familiar with C++.
  • Consolidate technical interview knowledge, especially on data structures and algorithms.
  • Polish resume and portfolio, become more confidence for interviews.

And ultimately, find an ideal job.

My study will under the supervision of Dave, one of my favorite faculty in ETC. Dave is experienced in game development, and he is super smart, also very nice to talk with. He has referred me some books to study. I am really appreciated for his help.

I will write down some posts in this blog as summaries during my study. Hope everything goes well.

Good luck to myself, keep calm and carry on.

骑车去诚品

我今天干了件伟大的事儿——把《巨流河》看完了。

伟大,真伟大,都伟大。

作者伟大——出生在一个伟大的时代,出自一个伟大的家庭,经历了伟大的故事,做了一些伟大的事儿。齐邦媛伟大的人生差不多见证了小半个中国近代史。

书伟大——内容翔实,情感丰富,纪录了横跨一个世纪的国事家事、工作生活、亲情友情。小半本近代中國,一整本真情流露。

读者伟大——六百多页,比新华字典还厚,我居然看完了。作为一个不常看书的人,这一本把前后两年的阅读量都补回来了。我由衷地佩服自己,我真伟大。

当然,伟大的故事通常要发生在伟大的地方。我能完成如此伟大的壮举,主要还是应该感谢诚品书店。

作为台湾的文化名片,来了一定要看看诚品书店。看看它的书目,它的陈列,它的氛围,它的读者。不说别的,诚品书店大概是为数不多的设有座位的书店了。店内环境优雅,气氛和善,大家都很安静,默默地走着或是看着,像是一个看书的而不是卖书的地方。经常有读者席地而坐,店员也没有任何要驱赶的意思。这大概解释了诚品书店为什么常年亏本,也是我能看完《巨流河》的主要原因吧。

诚品书店的名气很大,来之前我就听说了。我以为书店会很大,面积像名气一样大,起码得几层楼,藏书贯穿古今。来了以后发现,书店藏身于诚品的商场中,只有区区一层,略有失望。书目算是全面,但毕竟只有一层,所以也算不上丰富。以人文社科类为主,技术类理工类专业书籍点到为止,这也体现了诚品深深的文艺气息。但是,类似星巴克不见得咖啡有多好喝,而是它能提供一个安静喝咖啡的地方,诚品书店也能提供一个好好看书的地方,让你感觉进来了就成了一个文艺青年,端起一本书就成了一个文化人,就像点一杯星巴克好像就成了小资一样。诚品店虽不算很大,书也不算太多,但它营造出的这种「读书人」的感觉,才是它的成功之道吧。

刚一来台湾我就到访了诚品书店。并不是我有多爱读书,台北就这么大,诚品又这么多,走着走着就走到了诚品。刚开始我去的敦南店,貌似是最大的,24小时营业。一上来我就看到了韩寒的《出发》。这是他在台湾出的散文集,大多是博客文章收录,当然收录了一些被删除文章。一开始并没发觉有座位,便或站或蹲或坐在台阶上,连续去了三五回,把《出发》看完了。

说实话,看着很爽,起码很解气。韩寒把该说的都说了,该骂的都骂了,然并卵。骂完以后,爽完以后,现实依旧如此,文章该删就删,大家该干嘛还干嘛。此类文章的效果和AV差不多,用一时之爽麻痹长久之痛。然而放在台湾出版,像是隔岸观火。隔着海峡一顿批判,但只是头在对岸,身体却还在火中,看似事不关己却着实关己,骂完爽完,更多的是身处其中的痛苦。这么说还是和AV一样,爽完就会进入贤者模式,感到深深的空虚与失望。

好在资深文艺青年双瑞及时出现,推荐了几本纪实文学,《巨流河》、《甲骨文》与《大路》。这三本书在豆瓣上的评分都在9分左右,然而国内都买不到。这么一想,「查禁」简直成了一种认证,就像「GFW认证」一样,说明内容足够「有意思」。讲真,如果不被禁,我倒也不会去关注了,国内这么多书,我看过几本?这么一禁,反而勾起了我的好奇心,决定一读。

(突然感觉本站离「GFW认证」也不远了。^_^)

后来我经常去诚品信义店,因为在那里发现了一个安静的角落,有两排长凳。可能因为周围都是物理、机械和宗教类的书籍,人迹罕至,所以经常有空座。对面还是一扇玻璃逃生门,抬头便能看见101大楼,视野甚好。此后我日常的一天经常是这样度过:中午或下午起床,骑上YouBike,塞上耳机,一路南下到永康街,下车吃点东西,葱抓饼牛肉面越南河粉芒果冰,再来一杯天仁茗茶,跨上车继续向东。宽广的信义大道上,101在远处闪烁着刺眼的阳光,痛仰的「公路之歌」盖过身边机车的轰鸣,脚下的飞轮努力跟上耳边的节奏,眼前来来往往的面孔飞驰而过,在脑海中留下一个个善意的微笑。春风十里,阳光普照,一路开阔而舒畅。向着101而去,远看似乎只有一指宽高,到了跟前才发现根本望不到顶。在101下左拐,略过大法弟子,在市政府门前停车,身旁就是诚品信义店。轻车熟路地走进去,拿起一本《巨流河》,找到我的据点,坐下來,翻开书,再次回到半个世纪前的峥嵘岁月。再抬头时,眼前的101已华灯初上,在夜幕下闪着光。

在此还要表扬一下台湾的YouBike。微笑单车,这是台湾的公共自行车系统。网点齐全,就台北而言,几乎遍布全市,差不多几百米就有一个网点。操作方便,取车还车刷悠游卡即可。价格便宜,前半小时5元台币,之后每小时10元,相比于15元一次的公交车,YouBike是短途优选。车况也很好,都是捷安特的自行车,车座高度可以轻松调节,还带三档变速。如果发现有问题,就把车座向后转,定期会有人来排查。

我相信这个系统中不光是自行车与网点的维护,还包含了道路建设。台北的路,对自行车极为友善,主干道基本都有自行车道。在台北骑单车,几乎没有被台阶颠过屁股,因为所有的路口台阶处都修建了坡道。一句话,你觉得该有坡的地方,一定会有坡。上次我爸妈来,带他们感受了一番YouBike,一路顺畅,颇得好评。我妈感叹,家门口的店铺,为了防止车主停车堵门,硬是把马路牙子上原有的上坡给凿了,这样车就上不去了。相比于台北应有尽有的坡道,真是哭笑不得。

所以要感谢YouBike与诚品书店,让我这半年来看了些书。

如今回望,那些明媚下午,信义大道上的单车少年,向着101飞驰而去。一路容光焕发,享受着阳光、春风与音乐,欢笑着扑向书籍的海洋。这一幕,大概是我半年的台湾生活中,最充实又最惬意的记忆吧。

台湾美人

不好意思标题党了一回。

你以为我要说台湾的美女?图森破!不过讲道理这里美女的确很多,尤其是台北。或者说,起码这里的妹子都很会化妆。毕竟生活水准高了以后,人们就会也才会在意外貌着装。这个道理也适用于帝都三里屯地区。

那我要说美男吗?拿衣服!我这么直。当然这里帅哥也不少,道理同上。

我要说的是整体的台湾人民。

之前说过双瑞遇到的各种台湾好人,韩寒蒋方舟们也论证过台湾人的好。但是我还想来补一刀。

前几天小伙伴俊松来台湾玩,我尽地主之谊带他好吃好喝。我们去垦丁住的民宿,图便宜订了一个大床房,想着挤挤也能睡。到了以后老板娘看着我们两个大小伙子,笑笑说,反正今天没满房,还有个四人房空着,你们去睡那间吧!

打开房门,看见两个正方形大床,我俩都蒙逼了。还是他先回过神来,“真尼玛文明社会。”

下楼后老板帮我们租了电瓶车。车店就在民宿的隔壁,两家长期合作,关系不错,所以价格合理,服务完善。

第二天我们去玩水上项目,回来后一身沙。俊松浑身难受,穿着泳裤就冲上楼洗澡,结果把衣服落在车座里。他把钥匙给我,让我帮他下去拿。我下去找到他的电瓶车,插进钥匙,怎么也打不开座位盖。左拧拧,右拧拧,都不对。车店老板过来帮忙,拔出钥匙一看,断了。

我蒙逼了。连声向老板道歉,赶紧上楼找俊松。他一听也蒙逼了。我说我觉得你给我的时候就是断的,我插进去的时候就觉得不对。他说不会啊应该是你自己拧断的,我下车时还好好的。至于这钥匙到底是怎么断的,这已经成了一个谜。但是不管怎样还是赶紧下去认怂吧,态度好一点说不定还能少赔点钱。于是我俩就垂头丧气地下去认怂。心里想着要赔多少钱?人民币一百块以下都能接受吧,毕竟理亏,就算他要宰我们也没啥办法。

过去见了车店老板,他果然脸色很不好。白了我们一眼,“这么着急,裤子还没脱就硬上,哪行呢!”说完拿来一张“注意事项”,清楚明了的几行字,最后一条写着“钥匙丢失赔偿新台币500元”,我们租车前都看过还签了字。钥匙弄坏和弄丢效果是一样的,没什么可撕逼的,没辙,只能怂。于是俊松默默掏出了钱包。

“唉?那要这么多!算了,人家也是不小心嘛!”突然角落里传来一个天籁般的女声,让我们再次蒙逼。租车店的老板娘从屋里走出来,大手一挥盖在“新台币500元”上,说,“算了算了,也是不小心,打一把新的就是了,100元足够了!”说完盯着老板,眼色不容置疑。老板翻了个白眼,转过头去,默许了。

我俩更蒙逼了,这剧情反转的太快有点跟不上。突然又是一声天籁——

“唉不就一百块嘛,来,紘哥给了!”民宿老板不知从哪儿冒出来,直接从钱包里掏出一百块放在桌上。“刚才他开锁时我叫了他一声,想和他打个招呼呢,估计吓着他了,哈哈哈哈,怪我怪我。”他看看我,笑着说,“这个车型号比较老,钥匙的确不太好使,我应该提前和你说的,我忘了,所以不怪你,这钱你不用给了,紘哥赔了!”最后四个字铿锵有力,掷地有声,震得车店老板也蒙逼了。

我石化了,感觉空气都蒙逼了,时间都停滞了。这剧情如此精彩,让人怀疑这是不是他们之前就排练好的一场名叫《台湾好人》的情景剧,为的是让我们增加对台湾的好感。

见大家都不说话,紘哥又大笑起来:“这辆车是你们大陆产的唉,哈哈哈,好像质量的确不怎么样嘛。你看他的不就没断嘛,哈哈哈!”这爽朗的笑声瞬间融化了空气,解开了我的封印,车店老板也笑了起来。

车店老板收下钱,找来工具,打开车盖,敲敲打打,要把断在里面的碎片取出来。过程看起来很复杂,我和俊松想帮忙,却也帮不上什么忙,只好无地自容地站在旁边,默默注视着。老板抬头看看我们,问:“你们还要出去吗?那先换辆车吧,这个放在这里修就好了。”我俩赶忙摆手,说只是想看看有什么要帮忙的。老板白了我们一眼,“你们赶紧上去休息吧!在这里也帮不上什么忙,弄得我还不好意思。”我俩更囧了,明明是我们该不好意思啊。

回到民宿,我俩给紘哥90度的鞠躬,深表感激之情。紘哥又大手一挥,说不要想啦,小事情啦,趁着天气好赶紧去玩吧,开心就好!

我们哪好意思再跑出去玩!于是默默回到房间,躺在床上,等着修好车,也反思着刚才的剧情。蒙逼了一会儿,俊松默默冒出一句:“这辈子没见过这么好的人。”

“文明社会。”我说。还能说什么呢?

“果然最美的风景是人。”俊松又冒出一句。

第二天退房,紘嫂把我们送到公车站,我俩道不完的谢,鞠不完躬。紘嫂哈哈一笑,还顺手送了我们两个小纪念品。道别后我俩在车站默默等公车,突然面前停下一辆小黄。司机摇下车窗,问你们是回高雄吗?我说是,去高铁站。司机说你们买票了吗?我说我们上车刷卡。司机手一挥,说,上车吧,我正好要过去,算你们刷卡的钱,三百五就好了。我俩想想的确差不多,刷卡大概三百左右,相差不到十块钱人民币。但是公车停靠站多,比较慢,小黄速度快,节省时间,座位还舒适。一合计,走吧。

从垦丁到高雄大约两小时的车程,我闭上眼准备睡一会儿。突然手机在口袋里狂震,我一接是紘嫂。原来早上光顾着道谢鞠躬,房间钥匙忘了还了。我一摸果然还在我口袋里,蒙逼炸了。车已经开出去很远,不值得再回头了。紘嫂说,那你到高雄后找一个邮寄吧。

这时司机把手伸了过来。“钥匙忘了?”他笑笑,“你把钥匙给我吧,我经常跑这条线,我帮你还吧。”我有如遇到救命恩人一般,赶紧点头道谢,把钥匙递给他。紘嫂的电话还没挂,司机又接过去,“啊,春品啊,溢鑫边上的是吧,好,放心吧,我一会儿给你带回去啊。”

司机把电话还给我,看着一脸蒙逼的我,笑着说,这经常发生啦,哈哈,放心吧,我是固定跑这条线的,都熟悉,这条线上朋友也多,肯定能帮你还回去啦!说完就拿起车里的对讲机,用台语叽里哇啦说了一通。几番对话之后,他笑着说,搞定啦!我正蒙逼,只见他不一会儿就把车停到路对面,摇下车窗,旁边已经有另一辆车在等候。他把钥匙递过去,说了声,“春品”,这是我住的民宿的名字。另一辆车的司机给了个OK的手势,旋即飞驰而去。

原来司机通过无线电联系上了一位反方向的车主,从高雄到垦丁。两人约好地点交接,然后让他帮忙把钥匙捎了回去。

整个过程是如此顺利,就这么轻松随意地解决了我一个大问题,让我不得不又一次怀疑这是不是也是排练好的。

我却还是不放心,默默记下了司机的姓名和号码,以免有什么问题还可以追查。到了高铁站,司机如约收了我们每人三百五,并没有对还钥匙这项支线任务额外收费。打电话给紘哥,钥匙早已完璧归赵。司机笑着给了我们他的名片,说以后再来玩可以随时联系。我仔细看了看名片,他叫邬明煌,我刚才记得没错。我拿着他的名片,有一丝感动,因为他是如此善良、直率、乐于助人,也有一丝羞愧,因为我竟然还以小人之心揣度他的善举。

在回台北的高铁上,俊松不断地感叹,真是好人多啊。和我爸妈一样,他来时感叹台湾楼真矮,衰败破旧感觉落后;走时感叹台湾人真可爱,不吹不黑领先我们三十年。

我想起以前有人问过,谁是最可爱的人?

后来他自问自答。好吧,这么可爱说什么都对。

所以,也许台湾人成为不了最可爱的人,但是他们可以成为台湾最美的一道风景。

感谢台湾所有的美景与美人,愿你们一切都好。

 

文明社会

上周爸妈来台湾看望我,我就带他们游玩了一番。在为期一周的旅程中,出现次数最多的一个词,大概就是“文明社会”了。

这个词最早出自双瑞。刚到台湾的时候,每当我们处于“待宰”的状态下——遇到陌生人,去了陌生的地方,参与陌生的活动——我们都会习惯性地产生堤防之心——这里饭菜会不会不好?东西放这里会不会被偷?参加这个活动会不会被骗?这时双瑞就会大大咧咧来一句:“想什么呢,这里是文明社会!”

双瑞的确相信这里是文明社会。一次从地铁站出来,一个带着小孩的大叔找双瑞问路。双瑞也在找路,一脸蒙逼。于是大叔和他一起找,边走边聊。在得知他是陆生之后,问他,你觉得台湾怎么样?双瑞说,不错呀,风景好,环境好,空气好(作为河北人尤其喜爱这里的空气)。大叔笑了,台湾人也很好呀!有没有听说过台湾最美的风景是人?来来来,你还没吃饭吧,一起吃吧,好好聊聊!

于是双瑞莫名其妙地被这位陌生大叔请了一顿饭。晚上回来之后,他还处在蒙逼之中。和我们讲述了这段奇幻的经历,我们的第一反应竟是“陌生人的饭你都敢吃!不怕下毒吗!”结果被双瑞怒斥:“文明社会!”

还有一次双瑞去绿岛,坐出租车,司机一听是陆生,便聊起自己祖籍也是大陆,父辈来自陕西。双瑞说他来自河北,都是北方,离得不远。司机听说双瑞姓白,说自己在大陆有很多亲戚也姓白。双瑞笑笑说,估计我们还有点亲戚关系。车开到地方后,司机也下车,从后备箱里拿出一块大面包,强行塞给双瑞,说,都是自己人,人不亲土亲,相逢是缘,小兄弟祝你吃好玩好!

经历了这两次的奇幻的故事,“文明社会”一词就成为了双瑞化解我们“被害妄想症”的解药。在此次行程中,我也用它化解了爸妈的忧虑。

从下飞机开始,爸妈去洗手间,再三叮嘱我把行李看好,我笑笑,“文明社会!”;在垦丁,我们去玩水上活动,衣服裤子放在沙滩上觉得不放心,我就请工作人员帮忙收起来。小姑娘虽然有些不理解,但也帮我把手机放到了她自己的包里。我想起穿泳裤没带钱包,在透明的手机壳里塞了两千元钱,非常明显,看她塞进包里的那一瞬间还有些担忧。心里盘算着大不了钱你拿着手机得还我吧,结果上岸后完璧归赵,钱在手机壳里纹丝未动,我直感叹“文明社会!”;在嘉义的小饭馆吃鸡肉饭,爸妈总担心街边摊不干净,我又笑笑,指着墙上市长颁发的奖状,“文明社会!”;在港口的海鲜馆子吃饭,老爸怕被宰,点菜谨慎又紧张,结果最后一算便宜得惊人,感慨真是“文明社会”;在台北坐出租车,司机大叔和蔼又可爱,爸妈一路聊的喜笑颜开,下车连叹“文明社会”…

有太多的细节可以佐证这个社会的确够文明。这种文明体现在人与人之间最基本的信任,不用担心会遭受坑蒙拐骗,不用提心吊胆地去堤防每一个人,使我能以一个轻松舒适的心态去面对生活。心态是一种生活成本,习惯了文明社会中的简单快乐,回想起之前处处小心的日子,顿觉心累。当然,任何地方都不完美,哪里都有坏人,但我相信这是个概率的问题。只要好人的概率远远大于坏人,就是文明。我和双瑞把伞放在图书馆门口,出来时都没了。双瑞说可能是别人拿错了,我猜测可能是陆生干的。毕竟文明社会。

和我们一样,刚到的时候,爸妈总是问,这楼怎么这么矮,这房子怎么这么破,这街道怎么这么旧。慢慢的他们发现这些并不是什么缺点,甚至于喜欢上了这种感觉。回家之后,老爸发来了一篇文章,是韩寒那篇著名的《太平洋的风》。他大概在里面找到了答案:

我们所拥有的他们都拥有过,

我们所炫耀的他们的纳税人不会答应,

我们所失去的他们都留下了,

我们所缺少的,才是最能让人感到自豪的。