diff --git a/demos/TicTacToe/Classes/AppDelegate.cpp b/demos/TicTacToe/Classes/AppDelegate.cpp index 15f1ce66..6fd2ab92 100644 --- a/demos/TicTacToe/Classes/AppDelegate.cpp +++ b/demos/TicTacToe/Classes/AppDelegate.cpp @@ -1,3 +1,17 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #include "AppDelegate.h" #include "MainMenuScene.h" @@ -6,6 +20,7 @@ USING_NS_CC; // Set based on the image width. const float kFrameWidth = 600; + // Set based on the image height plus 40 for windows bar. const float kFrameHeight = 640; diff --git a/demos/TicTacToe/Classes/AppDelegate.h b/demos/TicTacToe/Classes/AppDelegate.h index 49ad5f31..75e7b832 100644 --- a/demos/TicTacToe/Classes/AppDelegate.h +++ b/demos/TicTacToe/Classes/AppDelegate.h @@ -1,3 +1,17 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #ifndef TICTACTOE_DEMO_CLASSES_APPDELEGATE_SCENE_H_ #define TICTACTOE_DEMO_CLASSES_APPDELEGATE_SCENE_H_ #include "cocos2d.h" diff --git a/demos/TicTacToe/Classes/MainMenuScene.cpp b/demos/TicTacToe/Classes/MainMenuScene.cpp index 297f3471..515ea26e 100644 --- a/demos/TicTacToe/Classes/MainMenuScene.cpp +++ b/demos/TicTacToe/Classes/MainMenuScene.cpp @@ -1,13 +1,82 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #include "MainMenuScene.h" +#include + #include "TicTacToeScene.h" static const char* kCreateGameImage = "create_game.png"; static const char* kTextFieldBorderImage = "text_field_border.png"; static const char* kJoinButtonImage = "join_game.png"; +static const char* kLoginButtonImage = "login.png"; +static const char* kLogoutButtonImage = "logout.png"; +static const char* kSignUpButtonImage = "sign_up.png"; +// Regex that will validate if the email entered is a valid email pattern. +const std::regex email_pattern("(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"); USING_NS_CC; +void LogMessage(const char* format, ...) { + va_list list; + va_start(list, format); + vprintf(format, list); + va_end(list); + printf("\n"); + fflush(stdout); +} + +void ProcessEvents(int msec) { +#ifdef _WIN32 + Sleep(msec); +#else + usleep(msec * 1000); +#endif // _WIN32 +} + +// Wait for a Future to be completed. If the Future returns an error, it will +// be logged. +void WaitForCompletion(const firebase::FutureBase& future, const char* name) { + while (future.status() == firebase::kFutureStatusPending) { + ProcessEvents(100); + } + if (future.status() != firebase::kFutureStatusComplete) { + LogMessage("ERROR: %s returned an invalid result.", name); + } else if (future.error() != 0) { + LogMessage("ERROR: %s returned error %d: %s", name, future.error(), + future.error_message()); + } +} + +// Returns a random uid of a specified length. +std::string GenerateUid(std::size_t length) { + const std::string kCharacters = "0123456789abcdefghjkmnpqrstuvwxyz"; + + std::random_device random_device; + std::mt19937 generator(random_device()); + std::uniform_int_distribution<> distribution(0, kCharacters.size() - 1); + + std::string generate_uid; + + for (std::size_t i = 0; i < length; ++i) { + generate_uid += kCharacters[distribution(generator)]; + } + + return generate_uid; +} + Scene* MainMenuScene::createScene() { // Builds a simple scene that uses the bottom left cordinate point as (0,0) // and can have sprites, labels and layers added onto it. @@ -22,35 +91,322 @@ bool MainMenuScene::init() { if (!Layer::init()) { return false; } - // Creates a sprite for the create button and sets its position to the center - // of the screen. TODO(grantpostma): Dynamically choose the location. - auto create_button = Sprite::create(kCreateGameImage); - create_button->setPosition(25, 200); - create_button->setAnchorPoint(Vec2(0, 0)); + + // Call the function to initialize the firebase features. + this->InitializeFirebase(); + + // Create the background to add all of the authentication elements on. The + // visiblity of this node should match kAuthState, disable any + // touch_listeners when not in this state. + auth_background_ = DrawNode::create(); + auto auth_background_border = DrawNode::create(); + + const auto auth_background_size = Size(500, 550); + const auto auth_background_origin = Size(50, 50); + Vec2 auth_background_corners[4]; + auth_background_corners[0] = + Vec2(auth_background_origin.width, auth_background_origin.height); + auth_background_corners[1] = + Vec2(auth_background_origin.width + auth_background_size.width, + auth_background_origin.height); + auth_background_corners[2] = + Vec2(auth_background_origin.width + auth_background_size.width, + auth_background_origin.height + auth_background_size.height); + auth_background_corners[3] = + Vec2(auth_background_origin.width, + auth_background_origin.height + auth_background_size.height); + + Color4F white(1, 1, 1, 1); + auth_background_->drawPolygon(auth_background_corners, 4, Color4F::BLACK, 1, + Color4F::BLACK); + auth_background_border->drawPolygon(auth_background_corners, 4, + Color4F(0, 0, 0, 0), 1, Color4F::WHITE); + auth_background_->addChild(auth_background_border); + + this->addChild(auth_background_, /*layer_index=*/10); + + // Label the background as Authentication. + auto auth_label = Label::createWithSystemFont("authentication", "Arial", 48); + auth_label->setPosition(Vec2(300, 550)); + auth_background_->addChild(auth_label); + + // Label to print out all of the login errors. + invalid_login_label_ = Label::createWithSystemFont("", "Arial", 24); + invalid_login_label_->setTextColor(Color4B::RED); + invalid_login_label_->setPosition(Vec2(300, 220)); + auth_background_->addChild(invalid_login_label_); + + // Label to display the users record. + user_record_label_ = Label::createWithSystemFont("", "Arial", 24); + user_record_label_->setAlignment(TextHAlignment::RIGHT); + user_record_label_->setTextColor(Color4B::WHITE); + user_record_label_->setPosition(Vec2(500, 600)); + this->addChild(user_record_label_); + + // Label for anonymous sign in. + auto anonymous_login_label = + Label::createWithSystemFont("anonymous sign-in", "Arial", 18); + anonymous_login_label->setTextColor(Color4B::WHITE); + anonymous_login_label->setPosition(Vec2(460, 75)); + + auto anonymous_label_touch_listener = EventListenerTouchOneByOne::create(); + + anonymous_label_touch_listener->onTouchBegan = + [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kAuthState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kAuthState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + if (bounds.containsPoint(point)) { + // Use anonymous sign in for the user. + user_result_ = auth_->SignInAnonymously(); + current_state_ = kWaitingAnonymousState; + } + + return true; + }; + + // Attach the touch listener to the text field. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(anonymous_label_touch_listener, + anonymous_login_label); + auth_background_->addChild(anonymous_login_label); + + // Extract the origin, size and position of the text field so that the + // border can be created based on those values. + const auto email_text_field_origin = Vec2(0, 0); + const auto email_text_field_position = Size(110, 350); + const auto text_field_padding = 20; + const auto email_font_size = 36.0; + const auto email_text_field_size = Size(400, 2 * email_font_size); + + // Set up the constraints of the border so it surrounds the text box. + Vec2 email_border_corners[4] = { + Vec2(email_text_field_position.width - text_field_padding, + email_text_field_position.height), + Vec2(email_text_field_position.width + email_text_field_size.width, + email_text_field_position.height), + Vec2(email_text_field_position.width + email_text_field_size.width, + email_text_field_position.height + email_text_field_size.height), + Vec2(email_text_field_position.width - text_field_padding, + email_text_field_position.height + email_text_field_size.height)}; + + // Create a text field border and add it around the text field. + auto email_text_field_border = DrawNode::create(); + email_text_field_border->drawPolygon(email_border_corners, 4, + Color4F(0, 0, 0, 0), 1, Color4F::WHITE); + + // Create a text field to enter the user's email. + email_text_field_ = cocos2d::TextFieldTTF::textFieldWithPlaceHolder( + "enter an email address", email_text_field_size, TextHAlignment::LEFT, + "Arial", email_font_size); + email_text_field_->setPosition(email_text_field_position); + email_text_field_->setAnchorPoint(Vec2(0, 0)); + email_text_field_->setColorSpaceHolder(Color3B::GRAY); + email_text_field_->setDelegate(this); + + auto email_text_field_touch_listener = EventListenerTouchOneByOne::create(); + + email_text_field_touch_listener->onTouchBegan = + [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kAuthState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kAuthState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + if (bounds.containsPoint(point)) { + // Show the on screen keyboard and places character inputs into the text + // field. + auto text_field = dynamic_cast(event->getCurrentTarget()); + text_field->setCursorEnabled(true); + text_field->attachWithIME(); + } else { + auto text_field = dynamic_cast(event->getCurrentTarget()); + text_field->setCursorEnabled(false); + text_field->detachWithIME(); + } + + return true; + }; + + // Attach the touch listener to the text field. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(email_text_field_touch_listener, + email_text_field_); + + auth_background_->addChild(email_text_field_, /*layer_index=*/1); + auth_background_->addChild(email_text_field_border, /*layer_index=*/1); + + // Extract the origin, size and position of the text field so that the + // border can be created based on those values. + const auto password_text_field_origin = Vec2(0, 0); + const auto password_text_field_position = Size(110, 250); + const auto password_font_size = 36.0; + const auto password_text_field_size = Size(400, 2 * password_font_size); + + // Set up the constraints of the border so it surrounds the text box. + Vec2 password_border_corners[4] = { + Vec2(password_text_field_position.width - text_field_padding, + password_text_field_position.height), + Vec2(password_text_field_position.width + password_text_field_size.width, + password_text_field_position.height), + Vec2(password_text_field_position.width + password_text_field_size.width, + password_text_field_position.height + + password_text_field_size.height), + Vec2(password_text_field_position.width - text_field_padding, + password_text_field_position.height + + password_text_field_size.height)}; + + // Create a text field border and add it around the text field. + auto password_text_field_border = DrawNode::create(); + password_text_field_border->drawPolygon( + password_border_corners, 4, Color4F(0, 0, 0, 0), 1, Color4F::WHITE); + + // Create a text field to enter the user's password. + password_text_field_ = cocos2d::TextFieldTTF::textFieldWithPlaceHolder( + "enter a password", password_text_field_size, TextHAlignment::LEFT, + "Arial", password_font_size); + password_text_field_->setPosition(password_text_field_position); + password_text_field_->setAnchorPoint(Vec2(0, 0)); + password_text_field_->setColorSpaceHolder(Color3B::GRAY); + password_text_field_->setSecureTextEntry(true); + password_text_field_->setDelegate(this); + + auto password_text_field_touch_listener = + EventListenerTouchOneByOne::create(); + + password_text_field_touch_listener->onTouchBegan = + [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kAuthState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kAuthState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + if (bounds.containsPoint(point)) { + // Shows the on screen keyboard and places character inputs into the text + // field. + auto text_field = dynamic_cast(event->getCurrentTarget()); + text_field->setCursorEnabled(true); + text_field->attachWithIME(); + } else { + auto text_field = dynamic_cast(event->getCurrentTarget()); + text_field->setCursorEnabled(false); + text_field->detachWithIME(); + } + + return true; + }; + + auth_background_->addChild(password_text_field_, /*layer_index=*/1); + auth_background_->addChild(password_text_field_border, /*layer_index=*/1); + + // Attach the touch listener to the text field. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority( + password_text_field_touch_listener, password_text_field_); + + // Create the login button and give it a position, anchor point and + // touch_listener. + auto login_button = Sprite::create(kLoginButtonImage); + login_button->setPosition(90, 120); + login_button->setAnchorPoint(Vec2(0, 0)); + login_button->setContentSize(Size(200, 75)); + // Create a button listener to handle the touch event. - auto create_button_touch_listener = EventListenerTouchOneByOne::create(); - // Setting the onTouchBegan event up to a lambda tha will replace the MainMenu - // scene with a TicTacToe scene. - create_button_touch_listener->onTouchBegan = [](Touch* touch, - Event* event) -> bool { - auto bounds = event->getCurrentTarget()->getBoundingBox(); - auto point = touch->getLocation(); - // Replaces the scene with a new TicTacToe scene if the touched point is - // within the bounds of the button. + auto login_button_touch_listener = EventListenerTouchOneByOne::create(); + + // Transition from kAuthState to kWaitingLoginState on button press and set + // user_result_ to SignInWithEmailAndPassword future result. + login_button_touch_listener->onTouchBegan = [this](Touch* touch, + Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kAuthState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kAuthState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); if (bounds.containsPoint(point)) { - Director::getInstance()->replaceScene( - TicTacToe::createScene(std::string())); + // Check the string for a valid email pattern. + if (!std::regex_match(email_text_field_->getString(), email_pattern)) { + invalid_login_label_->setString("invalid email address"); + } else if (password_text_field_->getString().length() < 8) { + invalid_login_label_->setString( + "password must be at least 8 characters long"); + } else { + user_result_ = auth_->SignInWithEmailAndPassword( + email_text_field_->getString().c_str(), + password_text_field_->getString().c_str()); + current_state_ = kWaitingLoginState; + } } + return true; + }; + + // Attach the touch listener to the login button. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(login_button_touch_listener, + login_button); + auth_background_->addChild(login_button, /*layer_index=*/1); + // Create the sign_up button and give it a position, anchor point and + // touch_listener. + auto sign_up_button = Sprite::create(kSignUpButtonImage); + sign_up_button->setPosition(310, 120); + sign_up_button->setAnchorPoint(Vec2(0, 0)); + sign_up_button->setContentSize(Size(200, 75)); + + // Create a button listener to handle the touch event. + auto sign_up_button_touch_listener = EventListenerTouchOneByOne::create(); + + // Transition from kAuthState to kWaitingSignUpState on button press and set + // user_result_ to CreateUserWithEmailAndPassword future result. + sign_up_button_touch_listener->onTouchBegan = [this](Touch* touch, + Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kAuthState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kAuthState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + if (bounds.containsPoint(point)) { + // Check the string for a valid email pattern. + if (!std::regex_match(email_text_field_->getString(), email_pattern)) { + invalid_login_label_->setString("invalid email address"); + } else if (password_text_field_->getString().length() < 8) { + invalid_login_label_->setString( + "password must be at least 8 characters long"); + } else { + user_result_ = auth_->CreateUserWithEmailAndPassword( + email_text_field_->getString().c_str(), + password_text_field_->getString().c_str()); + current_state_ = kWaitingSignUpState; + } + } return true; }; - // Attaching the touch listener to the create game button. + + // Attach the touch listener to the sign_up button. Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(create_button_touch_listener, - create_button); - // Creating, setting the position and assigning a placeholder to the text - // field for entering the join game uuid. + ->addEventListenerWithSceneGraphPriority(sign_up_button_touch_listener, + sign_up_button); + auth_background_->addChild(sign_up_button, /*layer_index=*/1); + + // Create, set the position and assign a placeholder to the text + // field for the user to enter the join game uuid. TextFieldTTF* join_text_field = cocos2d::TextFieldTTF::textFieldWithPlaceHolder( "code", cocos2d::Size(200, 100), TextHAlignment::LEFT, "Arial", 55.0); @@ -59,19 +415,25 @@ bool MainMenuScene::init() { join_text_field->setColorSpaceHolder(Color3B::WHITE); join_text_field->setDelegate(this); - auto text_field_border = Sprite::create(kTextFieldBorderImage); - text_field_border->setPosition(390, 50); - text_field_border->setAnchorPoint(Vec2(0, 0)); - text_field_border->setScale(.53f); - this->addChild(text_field_border, 0); - // Create a touch listener to handle the touch event. TODO(grantpostma): add a - // focus bar when selecting inside the text field's bounding box. - auto text_field_touch_listener = EventListenerTouchOneByOne::create(); - - text_field_touch_listener->onTouchBegan = - [join_text_field](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { - auto bounds = event->getCurrentTarget()->getBoundingBox(); - auto point = touch->getLocation(); + auto join_text_field_border = Sprite::create(kTextFieldBorderImage); + join_text_field_border->setPosition(390, 50); + join_text_field_border->setAnchorPoint(Vec2(0, 0)); + join_text_field_border->setScale(.53f); + this->addChild(join_text_field_border, /*layer_index=*/0); + + // Create a touch listener to handle the touch event. + auto join_text_field_touch_listener = EventListenerTouchOneByOne::create(); + + join_text_field_touch_listener->onTouchBegan = + [join_text_field, this](cocos2d::Touch* touch, + cocos2d::Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kGameMenuState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kGameMenuState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); if (bounds.containsPoint(point)) { // Show the on screen keyboard and places character inputs into the text // field. @@ -88,12 +450,93 @@ bool MainMenuScene::init() { return true; }; - // Attaching the touch listener to the text field. + // Attach the touch listener to the text field. Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(text_field_touch_listener, + ->addEventListenerWithSceneGraphPriority(join_text_field_touch_listener, join_text_field); + // Creates a sprite for the create button and sets its position to the + // center of the screen. TODO(grantpostma): Dynamically choose the location. + auto create_button = Sprite::create(kCreateGameImage); + create_button->setPosition(25, 200); + create_button->setAnchorPoint(Vec2(0, 0)); + + // Create a button listener to handle the touch event. + auto create_button_touch_listener = EventListenerTouchOneByOne::create(); + + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene. + create_button_touch_listener->onTouchBegan = + [this, join_text_field](Touch* touch, Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kGameMenuState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kGameMenuState) { + return false; + }; + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + + // Replaces the scene with a new TicTacToe scene if the touched point is + // within the bounds of the button. + if (bounds.containsPoint(point)) { + Director::getInstance()->pushScene( + TicTacToe::createScene(std::string(), database_, user_uid_)); + join_text_field->setString(""); + current_state_ = kWaitingGameOutcome; + } + + return true; + }; + + // Attach the touch listener to the create game button. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(create_button_touch_listener, + create_button); + + // Creates a sprite for the logout button and sets its position to the + auto logout_button = Sprite::create(kLogoutButtonImage); + logout_button->setPosition(25, 575); + logout_button->setAnchorPoint(Vec2(0, 0)); + logout_button->setContentSize(Size(125, 50)); + + // Create a button listener to handle the touch event. + auto logout_button_touch_listener = EventListenerTouchOneByOne::create(); + + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene. + logout_button_touch_listener->onTouchBegan = [this](Touch* touch, + Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kGameMenuState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kGameMenuState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + + // Replaces the scene with a new TicTacToe scene if the touched point is + // within the bounds of the button. + if (bounds.containsPoint(point)) { + current_state_ = kAuthState; + user_uid_ = ""; + user_ = nullptr; + invalid_login_label_->setString(""); + email_text_field_->setString(""); + password_text_field_->setString(""); + user_record_label_->setString(""); + } + + return true; + }; + + // Attach the touch listener to the logout game button. + Director::getInstance() + ->getEventDispatcher() + ->addEventListenerWithSceneGraphPriority(logout_button_touch_listener, + logout_button); + // Creates a sprite for the join button and sets its position to the center // of the screen. TODO(grantpostma): Dynamically choose the location and set // size(). @@ -101,36 +544,218 @@ bool MainMenuScene::init() { join_button->setPosition(25, 50); join_button->setAnchorPoint(Vec2(0, 0)); join_button->setScale(1.3f); + // Create a button listener to handle the touch event. auto join_button_touch_listener = EventListenerTouchOneByOne::create(); - // Setting the onTouchBegan event up to a lambda tha will replace the MainMenu - // scene with a TicTacToe scene and pass in join_text_field string. + + // Set the onTouchBegan event up to a lambda tha will replace the + // MainMenu scene with a TicTacToe scene and pass in join_text_field string. join_button_touch_listener->onTouchBegan = - [join_text_field](Touch* touch, Event* event) -> bool { - auto bounds = event->getCurrentTarget()->getBoundingBox(); - auto point = touch->getLocation(); + [join_text_field, this](Touch* touch, Event* event) -> bool { + // Returns false, not consuming the event, to exit the layer if + // current_state_ is not in the kGameMenuState or is switching states. + if (previous_state_ != current_state_ || current_state_ != kGameMenuState) { + return false; + } + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); if (bounds.containsPoint(point)) { - // Getting the string from join_text_field. + // Get the string from join_text_field. std::string join_text_field_string = join_text_field->getString(); if (join_text_field_string.length() == 4) { - Director::getInstance()->replaceScene( - TicTacToe::createScene(join_text_field_string)); + Director::getInstance()->pushScene(TicTacToe::createScene( + join_text_field_string, database_, user_uid_)); + current_state_ = kWaitingGameOutcome; + join_text_field->setString(""); } else { join_text_field->setString(""); } } return true; }; - // Attaching the touch listener to the join button. + + // Attach the touch listener to the join button. Director::getInstance() ->getEventDispatcher() ->addEventListenerWithSceneGraphPriority(join_button_touch_listener, join_button); - // Attaching the create button, join button and join text field to the + + // Attach the create button, join button and join text field to the // MainMenu scene. this->addChild(create_button); this->addChild(join_button); - this->addChild(join_text_field, 1); + this->addChild(logout_button); + this->addChild(join_text_field, /*layer_index=*/1); + + this->scheduleUpdate(); return true; } +// Initialize the firebase auth and database while also ensuring no dependencies +// are missing. +void MainMenuScene::InitializeFirebase() { + LogMessage("Initialize Firebase App."); + ::firebase::App* app; + +#if defined(_ANDROID_) + app = ::firebase::App::Create(GetJniEnv(), GetActivity()); +#else + app = ::firebase::App::Create(); +#endif // defined(ANDROID) + + LogMessage("Initialize Firebase Auth and Firebase Database."); + + // Use ModuleInitializer to initialize both Auth and Database, ensuring no + // dependencies are missing. + database_ = nullptr; + auth_ = nullptr; + void* initialize_targets[] = {&auth_, &database_}; + + const firebase::ModuleInitializer::InitializerFn initializers[] = { + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Auth."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = + ::firebase::auth::Auth::GetAuth(app, &result); + return result; + }, + [](::firebase::App* app, void* data) { + LogMessage("Attempt to initialize Firebase Database."); + void** targets = reinterpret_cast(data); + ::firebase::InitResult result; + *reinterpret_cast<::firebase::database::Database**>(targets[1]) = + ::firebase::database::Database::GetInstance(app, &result); + return result; + }}; + + ::firebase::ModuleInitializer initializer; + initializer.Initialize(app, initialize_targets, initializers, + sizeof(initializers) / sizeof(initializers[0])); + + WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); + + if (initializer.InitializeLastResult().error() != 0) { + LogMessage("Failed to initialize Firebase libraries: %s", + initializer.InitializeLastResult().error_message()); + ProcessEvents(2000); + } + LogMessage("Successfully initialized Firebase Auth and Firebase Database."); + + database_->set_persistence_enabled(true); +} + +// Updates the user record variables to reflect what is in the database. +void MainMenuScene::UpdateUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").GetValue(); + auto future_loses = ref_.Child("loses").GetValue(); + auto future_ties = ref_.Child("ties").GetValue(); + WaitForCompletion(future_wins, "getUserWinsData"); + WaitForCompletion(future_loses, "getUserLosesData"); + WaitForCompletion(future_ties, "getUserTiesData"); + user_wins_ = future_wins.result()->value().int64_value(); + user_loses_ = future_loses.result()->value().int64_value(); + user_ties_ = future_ties.result()->value().int64_value(); + user_record_label_->setString("W:" + to_string(user_wins_) + + " L:" + to_string(user_loses_) + + " T:" + to_string(user_ties_)); +} + +// Initialize the user records in the database. +void MainMenuScene::InitializeUserRecord() { + ref_ = database_->GetReference("users").Child(user_uid_); + auto future_wins = ref_.Child("wins").SetValue(0); + auto future_loses = ref_.Child("loses").SetValue(0); + auto future_ties = ref_.Child("ties").SetValue(0); + WaitForCompletion(future_wins, "setUserWinsData"); + WaitForCompletion(future_loses, "setUserLosesData"); + WaitForCompletion(future_ties, "setUserTiesData"); + user_wins_ = 0; + user_loses_ = 0; + user_ties_ = 0; + user_record_label_->setString("W:" + to_string(user_wins_) + + " L:" + to_string(user_loses_) + + " T:" + to_string(user_ties_)); +} + +// Overriding the onEnter method to update the user_record on reenter. +void MainMenuScene::onEnter() { + // if the scene enter is from the game, updateUserRecords and change + // current_state_. + if (current_state_ == kWaitingGameOutcome) { + this->UpdateUserRecord(); + current_state_ = kGameMenuState; + } + Layer::onEnter(); +} + +// Updates the previous_state_ when current_state_ != previous_state_: +// +// switch (current_state_) +// (1) kAuthState: makes the auth_background_ visable. +// (2) kGameMenuState: makes the auth_background_ invisable. +// (3) kWaitingAnonymousState: waits for anonymous sign in then swaps to (1). +// (4) kWaitingSignUpState: waits for sign up future completion, +// updates user variables, and swaps to (2). +// (5) kWaitingLoginState: waits for login future completion, +// updates user variables, and swaps to (2). +// (6) kWaitingGameOutcome: waits for director to pop the TicTacToeScene. +void MainMenuScene::update(float /*delta*/) { + if (current_state_ != previous_state_) { + if (current_state_ == kWaitingAnonymousState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = GenerateUid(10); + + this->InitializeUserRecord(); + + current_state_ = kGameMenuState; + } + } + } else if (current_state_ == kWaitingSignUpState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = user_->uid(); + + this->InitializeUserRecord(); + + current_state_ = kGameMenuState; + + } else { + // Change invalid_login_label_ to display the user_create failed. + invalid_login_label_->setString("invalid sign up"); + current_state_ = kAuthState; + } + } + } else if (current_state_ == kWaitingLoginState) { + if (user_result_.status() == firebase::kFutureStatusComplete) { + if (user_result_.error() == firebase::auth::kAuthErrorNone) { + user_ = *user_result_.result(); + user_uid_ = user_->uid(); + + this->UpdateUserRecord(); + + current_state_ = kGameMenuState; + } else { + // Change invalid_login_label_ to display the auth_result errored. + auto err = user_result_.error_message(); + invalid_login_label_->setString("invalid login"); + current_state_ = kAuthState; + } + } + } else if (current_state_ == kAuthState) { + // Sign out logic, adding auth screen. + auth_background_->setVisible(true); + user_ = nullptr; + previous_state_ = current_state_; + } else if (current_state_ == kGameMenuState) { + // Removes the authentication screen. + auth_background_->setVisible(false); + previous_state_ = current_state_; + } + } + return; +} diff --git a/demos/TicTacToe/Classes/MainMenuScene.h b/demos/TicTacToe/Classes/MainMenuScene.h index 28a78fb1..fd411d28 100644 --- a/demos/TicTacToe/Classes/MainMenuScene.h +++ b/demos/TicTacToe/Classes/MainMenuScene.h @@ -1,19 +1,100 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #ifndef TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ #define TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ #include "cocos2d.h" +#include "firebase/auth.h" +#include "firebase/database.h" +#include "firebase/future.h" +#include "firebase/util.h" #include "ui/CocosGUI.h" +using std::to_string; + +// TODO(grantpostma): Create a common util.h & util.cpp file. +void LogMessage(const char*, ...); +void ProcessEvents(int); +void WaitForCompletion(const firebase::FutureBase&, const char*); +std::string GenerateUid(std::size_t); class MainMenuScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { public: - // Builds a simple scene that uses the bottom left cordinate point as (0,0) + // Build a simple scene that uses the bottom left cordinate point as (0,0) // and can have sprites, labels and nodes added onto it. static cocos2d::Scene* createScene(); + + // The game loop method for this layer which runs every frame once scheduled + // using this->scheduleUpdate(). Acts as the state manager for this scene. + void MainMenuScene::update(float) override; + + // If the scene is re-entered from TicTacToeScene, then call + // UpdateUserRecord() and swap current_state_ to kGameMenuState. + void MainMenuScene::onEnter() override; + // Initializes the instance of a Node and returns a boolean based on if it was // successful in doing so. - virtual bool init(); - // Defines a create type for a specific type, in this case a Layer. + bool init() override; CREATE_FUNC(MainMenuScene); + + private: + // Defines the state the class is currently in, which updates the sprites in + // the MainMenuScene::update(float) method. + enum kSceneState { + kAuthState, + kGameMenuState, + kWaitingAnonymousState, + kWaitingSignUpState, + kWaitingLoginState, + kWaitingGameOutcome + }; + + // Updates the user record (wins,loses and ties) and displays it to the + // screen. + void MainMenuScene::UpdateUserRecord(); + + // Initializes the user record (wins,loses and ties) and displays it to the + // screen. + void MainMenuScene::InitializeUserRecord(); + // Initializes the the firebase app, auth, and database. + void MainMenuScene::InitializeFirebase(); + + // Node to be used as a background for the authentication menu. + cocos2d::DrawNode* auth_background_; + + // Labels and textfields for the authentication menu. + cocos2d::Label* invalid_login_label_; + cocos2d::Label* user_record_label_; + cocos2d::TextFieldTTF* email_text_field_; + cocos2d::TextFieldTTF* password_text_field_; + + // Variable to track the current state and previous state to check against to + // see if the state changed. + kSceneState current_state_ = kAuthState; + kSceneState previous_state_ = kAuthState; + + // User record variabales that are stored in firebase database. + int user_wins_; + int user_loses_; + int user_ties_; + + std::string user_uid_; + firebase::auth::User* user_; + firebase::Future user_result_; + firebase::database::Database* database_; + firebase::auth::Auth* auth_; + firebase::database::DatabaseReference ref_; }; #endif // TICTACTOE_DEMO_CLASSES_MAINMENU_SCENE_H_ diff --git a/demos/TicTacToe/Classes/TicTacToeLayer.cpp b/demos/TicTacToe/Classes/TicTacToeLayer.cpp index 03334b6e..f903966f 100644 --- a/demos/TicTacToe/Classes/TicTacToeLayer.cpp +++ b/demos/TicTacToe/Classes/TicTacToeLayer.cpp @@ -1,30 +1,18 @@ -#include "TicTacToeLayer.h" +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -#include "MainMenuScene.h" -#include "TicTacToeScene.h" -#include "firebase/app.h" -#include "firebase/auth.h" -#include "firebase/database.h" -#include "firebase/future.h" -#include "firebase/util.h" - -// Thin OS abstraction layer. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using firebase::database::DataSnapshot; -using firebase::database::MutableData; -using firebase::database::TransactionResult; -using std::string; +#include "TicTacToeLayer.h" USING_NS_CC; @@ -41,6 +29,11 @@ static const enum kGameOutcome { kGameTied, kGameDisbanded }; + +// create an array that has indicies of enum kGameOutcome and maps that to the +// database outcome key. +static const const char* kGameOutcomeStrings[] = {"wins", "loses", "ties", + "disbanded"}; static const std::array kGameOverStrings = { "you won!", "you lost.", "you tied.", "user left."}; @@ -48,37 +41,22 @@ static const std::array kGameOverStrings = { extern const int kTilesX; extern const int kTilesY; static const int kNumberOfTiles = kTilesX * kTilesY; + // Screen dimensions. static const double kScreenWidth = 600; static const double kScreenHeight = 600; static const double kTileWidth = (kScreenWidth / kTilesX); static const double kTileHeight = (kScreenHeight / kTilesY); + // The screen will display the end game text for 2 seconds (120frames/60fps). static const int kEndGameFramesMax = 120; + // Image file paths. static const char* kBoardImageFileName = "tic_tac_toe_board.png"; static const char* kLeaveButtonFileName = "leave_button.png"; static std::array kPlayerTokenFileNames = { "tic_tac_toe_x.png", "tic_tac_toe_o.png"}; -void LogMessage(const char* format, ...) { - va_list list; - va_start(list, format); - vprintf(format, list); - va_end(list); - printf("\n"); - fflush(stdout); -} - -bool ProcessEvents(int msec) { -#ifdef _WIN32 - Sleep(msec); -#else - usleep(msec * 1000); -#endif // _WIN32 - return false; -} - // An example of a ValueListener object. This specific version will // simply log every value it sees, and store them in a list so we can // confirm that all values were received. @@ -152,7 +130,8 @@ class SampleChildListener : public firebase::database::ChildListener { } public: - // Vector of strings defining the events we saw, in order. + // Vector of strings that contains the events in the order in which they + // occurred. std::vector events_; }; @@ -183,35 +162,6 @@ class ExpectValueListener : public firebase::database::ValueListener { bool got_value_; }; -// Wait for a Future to be completed. If the Future returns an error, it will -// be logged. -void WaitForCompletion(const firebase::FutureBase& future, const char* name) { - while (future.status() == firebase::kFutureStatusPending) { - ProcessEvents(100); - } - if (future.status() != firebase::kFutureStatusComplete) { - LogMessage("ERROR: %s returned an invalid result.", name); - } else if (future.error() != 0) { - LogMessage("ERROR: %s returned error %d: %s", name, future.error(), - future.error_message()); - } -} -std::string GenerateGameUuid(std::size_t length) { - const std::string kCharacters = "0123456789abcdefghjkmnpqrstuvwxyz"; - - std::random_device random_device; - std::mt19937 generator(random_device()); - std::uniform_int_distribution<> distribution(0, kCharacters.size() - 1); - - std::string GenerateGameUuid; - - for (std::size_t i = 0; i < length; ++i) { - GenerateGameUuid += kCharacters[distribution(generator)]; - } - - return GenerateGameUuid; -} - // A function that returns true if any of the row // is crossed with the same player's move static bool RowCrossed(int board[][kTilesY]) { @@ -254,106 +204,47 @@ static bool GameOver(int board[][kTilesY]) { return (RowCrossed(board) || ColumnCrossed(board) || DiagonalCrossed(board)); } -TicTacToeLayer::TicTacToeLayer(string game_uuid) { - join_game_uuid = game_uuid; - current_player_index = kPlayerOne; - game_outcome = kGameWon; - LogMessage("Initialized Firebase App."); - auto app = ::firebase::App::Create(); - LogMessage("Initialize Firebase Auth and Firebase Database."); - // Use ModuleInitializer to initialize both Auth and Database, ensuring no - // dependencies are missing. - firebase::ModuleInitializer initializer; - - database = nullptr; - auth = nullptr; - void* initialize_targets[] = {&auth, &database}; - - const firebase::ModuleInitializer::InitializerFn initializers[] = { - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Auth."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::auth::Auth**>(targets[0]) = - ::firebase::auth::Auth::GetAuth(app, &result); - return result; - }, - [](::firebase::App* app, void* data) { - LogMessage("Attempt to initialize Firebase Database."); - void** targets = reinterpret_cast(data); - ::firebase::InitResult result; - *reinterpret_cast<::firebase::database::Database**>(targets[1]) = - ::firebase::database::Database::GetInstance(app, &result); - return result; - }}; - - initializer.Initialize(app, initialize_targets, initializers, - sizeof(initializers) / sizeof(initializers[0])); - - WaitForCompletion(initializer.InitializeLastResult(), "Initialize"); - - if (initializer.InitializeLastResult().error() != 0) { - LogMessage("Failed to initialize Firebase libraries: %s", - initializer.InitializeLastResult().error_message()); - ProcessEvents(2000); - return; - } - LogMessage("Successfully initialized Firebase Auth and Firebase Database."); - - database->set_persistence_enabled(true); - - // Sign in using Auth before accessing the database. - // The default Database permissions allow anonymous users access. This will - // work as long as your project's Authentication permissions allow anonymous - // sign-in. - { - firebase::Future sign_in_future = - auth->SignInAnonymously(); - WaitForCompletion(sign_in_future, "SignInAnonymously"); - if (sign_in_future.error() == firebase::auth::kAuthErrorNone) { - LogMessage("Auth: Signed in anonymously."); - } else { - LogMessage("ERROR: Could not sign in anonymously. Error %d: %s", - sign_in_future.error(), sign_in_future.error_message()); - LogMessage( - " Ensure your application has the Anonymous sign-in provider " - "enabled in Firebase Console."); - LogMessage( - " Attempting to connect to the database anyway. This may fail " - "depending on the security settings."); - } - } - // Splits on the if depending on if the player created or joined the game. - // Additionally sets the player_index and total_players based on joining or - // creating a game. - if (join_game_uuid.empty()) { - join_game_uuid = GenerateGameUuid(4); - ref = database->GetReference("game_data").Child(join_game_uuid); +TicTacToeLayer::TicTacToeLayer(string game_uuid, + firebase::database::Database* main_menu_database, + string main_menu_user) { + join_game_uuid_ = game_uuid; + current_player_index_ = kPlayerOne; + game_outcome_ = kGameWon; + database_ = main_menu_database; + user_uid_ = main_menu_user; + + // If the join_game_uuid_ is present, initialize game variables, otherwise + // alter the game variables to signify a user joined. Additionally sets the + // player_index_ and total_players based on joining or creating a game. + if (join_game_uuid_.empty()) { + join_game_uuid_ = GenerateUid(4); + ref_ = database_->GetReference("game_data").Child(join_game_uuid_); firebase::Future future_create_game = - ref.Child("total_players").SetValue(1); - future_current_player_index = - ref.Child("current_player_index").SetValue(kPlayerOne); - future_game_over = ref.Child("game_over").SetValue(false); - WaitForCompletion(future_game_over, "setGameOver"); - WaitForCompletion(future_current_player_index, "setCurrentPlayerIndex"); + ref_.Child("total_players").SetValue(1); + future_current_player_index_ = + ref_.Child("current_player_index_").SetValue(kPlayerOne); + future_game_over_ = ref_.Child("game_over").SetValue(false); + WaitForCompletion(future_game_over_, "setGameOver"); + WaitForCompletion(future_current_player_index_, "setCurrentPlayerIndex"); WaitForCompletion(future_create_game, "createGame"); - player_index = kPlayerOne; - awaiting_opponenet_move = false; + player_index_ = kPlayerOne; + awaiting_opponenet_move_ = false; } else { - // Checks whether the join_uuid map exists. If it does not it set the - // initialization to failed. + // Checks whether the join_uuid map exists. If it does not then set + // the initialization to failed. auto future_game_uuid = - database->GetReference("game_data").Child(join_game_uuid).GetValue(); + database_->GetReference("game_data").Child(join_game_uuid_).GetValue(); WaitForCompletion(future_game_uuid, "GetGameDataMap"); auto game_uuid_snapshot = future_game_uuid.result(); if (!game_uuid_snapshot->value().is_map()) { - initialization_failed = true; + initialization_failed_ = true; } else { - ref = database->GetReference("game_data").Child(join_game_uuid); + ref_ = database_->GetReference("game_data").Child(join_game_uuid_); auto future_increment_total_users = - ref.RunTransaction([](MutableData* data) { + ref_.RunTransaction([](MutableData* data) { auto total_players = data->Child("total_players").value(); - // Completing the transaction based on the returned mutable data + + // Complete the transaction based on the returned mutable data // value. if (total_players.is_null()) { // Must return this if the transaction was unsuccessful. @@ -365,157 +256,168 @@ TicTacToeLayer::TicTacToeLayer(string game_uuid) { return TransactionResult::kTransactionResultAbort; } data->Child("total_players").set_value(new_total_players); + // Must call this if the transaction was successful. return TransactionResult::kTransactionResultSuccess; }); WaitForCompletion(future_increment_total_users, "JoinGameTransaction"); - player_index = kPlayerTwo; - awaiting_opponenet_move = true; + player_index_ = kPlayerTwo; + awaiting_opponenet_move_ = true; } } - // Creating the board sprite , setting the position to the bottom left of the - // frame (0,0), and finally moving the anchor point from the center of the + // Create the board sprite , set the position to the bottom left of the + // frame (0,0), and finally move the anchor point from the center of the // image(default) to the bottom left, Vec2(0.0,0.0). - board_sprite = Sprite::create(kBoardImageFileName); - if (!board_sprite) { + board_sprite_ = Sprite::create(kBoardImageFileName); + if (!board_sprite_) { log("kBoardImageFileName: %s file not found.", kBoardImageFileName); exit(true); } - board_sprite->setPosition(0, 0); - board_sprite->setAnchorPoint(Vec2(0.0, 0.0)); + board_sprite_->setPosition(0, 0); + board_sprite_->setAnchorPoint(Vec2(0.0, 0.0)); - leave_button_sprite = Sprite::create(kLeaveButtonFileName); - if (!leave_button_sprite) { + leave_button_sprite_ = Sprite::create(kLeaveButtonFileName); + if (!leave_button_sprite_) { log("kLeaveButtonSprite: %s file not found.", kLeaveButtonFileName); exit(true); } - leave_button_sprite->setPosition(450, 585); - leave_button_sprite->setAnchorPoint(Vec2(0.0, 0.0)); - leave_button_sprite->setScale(.35); + leave_button_sprite_->setPosition(450, 585); + leave_button_sprite_->setAnchorPoint(Vec2(0.0, 0.0)); + leave_button_sprite_->setScale(.35); // Create a button listener to handle the touch event. auto leave_button_sprite_touch_listener = EventListenerTouchOneByOne::create(); - // Setting the onTouchBegan event up to a lambda will swap scenes and modify + + // Set the onTouchBegan event up to a lambda will swap scenes and modify // total_players leave_button_sprite_touch_listener->onTouchBegan = [this](Touch* touch, Event* event) -> bool { - auto bounds = event->getCurrentTarget()->getBoundingBox(); - auto point = touch->getLocation(); + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + const auto point = touch->getLocation(); + // Replaces the scene with a new TicTacToe scene if the touched point is // within the bounds of the button. if (bounds.containsPoint(point)) { - // Update the game_outcome to reflect if the you rage quit or left + // Update the game_outcome_ to reflect if the you rage quit or left // pre-match. - if (remaining_tiles.size() == kNumberOfTiles) { - game_outcome = kGameDisbanded; + if (remaining_tiles_.size() == kNumberOfTiles) { + game_outcome_ = kGameDisbanded; } else { - game_outcome = kGameLost; + game_outcome_ = kGameLost; } - WaitForCompletion(ref.Child("game_over").SetValue(true), "setGameOver"); + WaitForCompletion(ref_.Child("game_over").SetValue(true), "setGameOver"); } return true; }; - // Attaching the touch listener to the create game button. + + // Attach the touch listener to the create game button. Director::getInstance() ->getEventDispatcher() ->addEventListenerWithSceneGraphPriority( - leave_button_sprite_touch_listener, leave_button_sprite); + leave_button_sprite_touch_listener, leave_button_sprite_); - board_sprite->addChild(leave_button_sprite, 1); + board_sprite_->addChild(leave_button_sprite_, /*layer_index=*/1); // TODO(grantpostma@): Modify these numbers to be based on the extern window // size & label size dimensions. cocos2d::Label* game_uuid_label = - Label::createWithSystemFont(join_game_uuid, "Arial", 30); + Label::createWithSystemFont(join_game_uuid_, "Arial", 30); game_uuid_label->setPosition(Vec2(40, 20)); - board_sprite->addChild(game_uuid_label, 1); - waiting_label = Label::createWithSystemFont("waiting", "Arial", 30); - waiting_label->setPosition(Vec2(530, 20)); + board_sprite_->addChild(game_uuid_label, /*layer_index=*/1); + waiting_label_ = Label::createWithSystemFont("waiting", "Arial", 30); + waiting_label_->setPosition(Vec2(530, 20)); - board_sprite->addChild(waiting_label, 1); - game_over_label = Label::createWithSystemFont("", "Arial", 80); - game_over_label->setPosition(Vec2(300, 300)); - board_sprite->addChild(game_over_label, 1); + board_sprite_->addChild(waiting_label_, /*layer_index=*/1); + game_over_label_ = Label::createWithSystemFont("", "Arial", 80); + game_over_label_->setPosition(Vec2(300, 300)); + board_sprite_->addChild(game_over_label_, /*layer_index=*/1); - // total_player_listener and CurrentPlayerIndexListener listener is set up + // total_player_listener_ and CurrentPlayerIndexListener listener is set up // to recognise when the desired players have connected & when turns - // alternate - LogMessage("total_player_listener"); - total_player_listener = + // alternate. + total_player_listener_ = std::make_unique(kNumberOfPlayers); - game_over_listener = std::make_unique(true); - - current_player_index_listener = std::make_unique(); - last_move_listener = std::make_unique(); - LogMessage("%i", total_player_listener->got_value()); - ref.Child("total_players").AddValueListener(total_player_listener.get()); - ref.Child("game_over").AddValueListener(game_over_listener.get()); - ref.Child("current_player_index") - .AddValueListener(current_player_index_listener.get()); - ref.Child("last_move").AddValueListener(last_move_listener.get()); - - // A 3*3 Tic-Tac-Toe board for playing + game_over_listener_ = std::make_unique(true); + + current_player_index_listener_ = std::make_unique(); + last_move_listener_ = std::make_unique(); + ref_.Child("total_players").AddValueListener(total_player_listener_.get()); + ref_.Child("game_over").AddValueListener(game_over_listener_.get()); + ref_.Child("current_player_index_") + .AddValueListener(current_player_index_listener_.get()); + ref_.Child("last_move").AddValueListener(last_move_listener_.get()); + + // Set up a 3*3 Tic-Tac-Toe board for tracking results. for (int i = 0; i < kTilesY; i++) { for (int j = 0; j < kTilesX; j++) { board[i][j] = kEmptyTile; - remaining_tiles.insert((i * kTilesX) + j); + remaining_tiles_.insert((i * kTilesX) + j); }; } - // Adding a function to determine which tile was selected to the onTouchBegan + + // Add a function to determine which tile was selected to the onTouchBegan // listener. auto touch_listener = EventListenerTouchOneByOne::create(); touch_listener->onTouchBegan = [this](Touch* touch, Event* event) mutable -> bool { - if (!total_player_listener->got_value()) return true; - if (current_player_index_listener->last_seen_value() != player_index) + if (!total_player_listener_->got_value()) return true; + if (current_player_index_listener_->last_seen_value() != player_index_) return true; - auto bounds = event->getCurrentTarget()->getBoundingBox(); - // Checking to make sure the touch location is within the bounds of the + const auto bounds = event->getCurrentTarget()->getBoundingBox(); + + // Check to make sure the touch location is within the bounds of the // board. if (bounds.containsPoint(touch->getLocation())) { // Calculates the tile number [0-8] which corresponds to the touch // location. int selected_tile = floor(touch->getLocation().x / kTileWidth) + kTilesX * floor(touch->getLocation().y / kTileHeight); - if (remaining_tiles.find(selected_tile) == remaining_tiles.end()) + if (remaining_tiles_.find(selected_tile) == remaining_tiles_.end()) return true; - auto sprite = Sprite::create(kPlayerTokenFileNames[current_player_index]); + auto sprite = + Sprite::create(kPlayerTokenFileNames[current_player_index_]); if (sprite == NULL) { log("kPlayerTokenFileNames: %s file not found.", - kPlayerTokenFileNames[current_player_index]); + kPlayerTokenFileNames[current_player_index_]); exit(true); } + // Calculates and sets the position of the sprite based on the // move_tile and the constant screen variables. sprite->setPosition((.5 + selected_tile % kTilesX) * kTileWidth, (.5 + selected_tile / kTilesY) * kTileHeight); - board_sprite->addChild(sprite); - // Modifying local game state variables to reflect this most recent move + board_sprite_->addChild(sprite); + + // Modify local game state variables to reflect this most recent move board[selected_tile / kTilesX][selected_tile % kTilesX] = - current_player_index; - remaining_tiles.erase(selected_tile); - current_player_index = (current_player_index + 1) % kNumberOfPlayers; - future_last_move = ref.Child("last_move").SetValue(selected_tile); - future_current_player_index = - ref.Child("current_player_index").SetValue(current_player_index); - WaitForCompletion(future_last_move, "setLastMove"); - WaitForCompletion(future_current_player_index, "setCurrentPlayerIndex"); - awaiting_opponenet_move = true; - waiting_label->setString("waiting"); + current_player_index_; + remaining_tiles_.erase(selected_tile); + current_player_index_ = (current_player_index_ + 1) % kNumberOfPlayers; + future_last_move_ = ref_.Child("last_move").SetValue(selected_tile); + future_current_player_index_ = + ref_.Child("current_player_index_").SetValue(current_player_index_); + WaitForCompletion(future_last_move_, "setLastMove"); + WaitForCompletion(future_current_player_index_, "setCurrentPlayerIndex"); + awaiting_opponenet_move_ = true; + waiting_label_->setString("waiting"); if (GameOver(board)) { - // Set game_over_label to reflect the use won. - game_over_label->setString("you won!"); - } else if (remaining_tiles.size() == 0) { - // Update game_outcome to reflect the user tied. - WaitForCompletion(ref.Child("game_over").SetValue(true), "setGameOver"); - game_outcome = kGameTied; + // Set game_over_label_ to reflect the user won. + game_over_label_->setString("you won!"); + game_outcome_ = kGameWon; + WaitForCompletion(ref_.Child("game_over").SetValue(true), + "setGameOver"); + } else if (remaining_tiles_.size() == 0) { + // Update game_outcome_ to reflect the user tied. + game_outcome_ = kGameTied; + WaitForCompletion(ref_.Child("game_over").SetValue(true), + "setGameOver"); } } return true; @@ -523,81 +425,105 @@ TicTacToeLayer::TicTacToeLayer(string game_uuid) { Director::getInstance() ->getEventDispatcher() - ->addEventListenerWithSceneGraphPriority(touch_listener, board_sprite); + ->addEventListenerWithSceneGraphPriority(touch_listener, board_sprite_); + + this->addChild(board_sprite_); - this->addChild(board_sprite); // Schedule the update method for this scene. this->scheduleUpdate(); } // Called automatically every frame. The update is scheduled in constructor. void TicTacToeLayer::update(float /*delta*/) { - // Replacing the scene with MainMenuScene if the initialization fails. - if (initialization_failed == true) { - Director::getInstance()->replaceScene(MainMenuScene::createScene()); + // Replace the scene with MainMenuScene if the initialization fails. + if (initialization_failed_ == true) { + Director::getInstance()->popScene(); } + // Performs the actions of the other player when the - // current_player_index_listener is equal to the player index. - else if (current_player_index_listener->last_seen_value() == player_index && - awaiting_opponenet_move == true) { + // current_player_index_listener_ is equal to the player index. + else if (current_player_index_listener_->last_seen_value() == player_index_ && + awaiting_opponenet_move_ == true) { int last_move = - last_move_listener->last_seen_value().AsInt64().int64_value(); - // Placing the players move on the board. - board[last_move / kTilesX][last_move % kTilesX] = current_player_index; - // Removing the tile from the tile unordered set. - remaining_tiles.erase(last_move); - auto sprite = Sprite::create(kPlayerTokenFileNames[current_player_index]); + last_move_listener_->last_seen_value().AsInt64().int64_value(); + + // Place the players move on the board. + board[last_move / kTilesX][last_move % kTilesX] = current_player_index_; + + // Remove the tile from the tile unordered set. + remaining_tiles_.erase(last_move); + auto sprite = Sprite::create(kPlayerTokenFileNames[current_player_index_]); if (sprite == NULL) { log("kPlayerTokenFileNames: %s file not found.", - kPlayerTokenFileNames[current_player_index]); + kPlayerTokenFileNames[current_player_index_]); exit(true); } + // Calculates and sets the position of the sprite based on the // move_tile and the constant screen variables. sprite->setPosition((.5 + last_move % kTilesX) * kTileWidth, (.5 + last_move / kTilesY) * kTileHeight); - board_sprite->addChild(sprite); - // Modifying local game state variables to reflect this most recent move. - board[last_move / kTilesX][last_move % kTilesX] = current_player_index; - remaining_tiles.erase(last_move); - awaiting_opponenet_move = false; - current_player_index = player_index; + board_sprite_->addChild(sprite); + + // Modify local game state variables to reflect this most recent move. + board[last_move / kTilesX][last_move % kTilesX] = current_player_index_; + remaining_tiles_.erase(last_move); + awaiting_opponenet_move_ = false; + current_player_index_ = player_index_; if (GameOver(board)) { - // Set game_outcome to reflect the use lost. - game_outcome = kGameLost; - } else if (remaining_tiles.size() == 0) { - // Set game_outcome to reflect the game ended in a tie. - game_outcome = kGameTied; + // Set game_outcome_ to reflect the use lost. + game_outcome_ = kGameLost; + WaitForCompletion(ref_.Child("game_over").SetValue(true), "setGameOver"); + } else if (remaining_tiles_.size() == 0) { + // Set game_outcome_ to reflect the game ended in a tie. + game_outcome_ = kGameTied; + WaitForCompletion(ref_.Child("game_over").SetValue(true), "setGameOver"); } } + // Shows the end game label for kEndGameFramesMax to show the result of the // game. - else if (game_over_listener->got_value()) { - if (game_outcome == kGameDisbanded && - remaining_tiles.size() != kNumberOfTiles) { - game_outcome = kGameWon; + else if (game_over_listener_->got_value()) { + if (game_outcome_ == kGameDisbanded && + remaining_tiles_.size() != kNumberOfTiles) { + game_outcome_ = kGameWon; } - game_over_label->setString(kGameOverStrings[game_outcome]); - end_game_frames++; - if (end_game_frames > kEndGameFramesMax) { - // TODO(grantpostma): Update authenticated users record. - WaitForCompletion(database->GetReference("game_data") - .Child(join_game_uuid) + game_over_label_->setString(kGameOverStrings[game_outcome_]); + end_game_frames_++; + if (end_game_frames_ > kEndGameFramesMax) { + // Remove the game from existence and update the user's record before + // swap back scenes. + WaitForCompletion(database_->GetReference("game_data") + .Child(join_game_uuid_) .RemoveValue(), "removeGameUuid"); - Director::getInstance()->replaceScene(MainMenuScene::createScene()); + ref_ = database_->GetReference("users").Child(user_uid_); + + // Updates user record unless the game was disbanded. + if (game_outcome_ != kGameDisbanded) { + auto future_record = + ref_.Child(kGameOutcomeStrings[game_outcome_]).GetValue(); + WaitForCompletion(future_record, "getPreviousOutcomeRecord"); + WaitForCompletion( + ref_.Child(kGameOutcomeStrings[game_outcome_]) + .SetValue(future_record.result()->value().int64_value() + 1), + "setGameOutcomeRecord"); + } + + Director::getInstance()->popScene(); } } + // Updates the waiting label to signify it is this players move. - else if (total_player_listener->got_value() && - awaiting_opponenet_move == false) { - waiting_label->setString("your move"); + else if (total_player_listener_->got_value() && + awaiting_opponenet_move_ == false) { + waiting_label_->setString("your move"); } } TicTacToeLayer::~TicTacToeLayer() { // release our sprite and layer so that it gets deallocated - CC_SAFE_RELEASE_NULL(this->board_sprite); - CC_SAFE_RELEASE_NULL(this->game_over_label); - CC_SAFE_RELEASE_NULL(this->waiting_label); + CC_SAFE_RELEASE_NULL(this->board_sprite_); + CC_SAFE_RELEASE_NULL(this->game_over_label_); + CC_SAFE_RELEASE_NULL(this->waiting_label_); } diff --git a/demos/TicTacToe/Classes/TicTacToeLayer.h b/demos/TicTacToe/Classes/TicTacToeLayer.h index 1fa708b0..b8b645d2 100644 --- a/demos/TicTacToe/Classes/TicTacToeLayer.h +++ b/demos/TicTacToe/Classes/TicTacToeLayer.h @@ -1,15 +1,35 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #ifndef TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ #define TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include "MainMenuScene.h" #include "TicTacToeScene.h" #include "cocos2d.h" -#include "firebase/app.h" -#include "firebase/auth.h" -#include "firebase/database.h" -#include "firebase/future.h" -#include "firebase/util.h" using cocos2d::Director; using cocos2d::Event; @@ -22,57 +42,76 @@ using firebase::Future; using firebase::database::DataSnapshot; using firebase::database::MutableData; using firebase::database::TransactionResult; +using std::string; +// Tile Constants. static const int kTilesX = 3; static const int kTilesY = 3; + +// Firebase listeners. class SampleValueListener; class ExpectValueListener; -class TicTacToeLayer : public Layer { - private: - typedef TicTacToeLayer self; - typedef Layer super; +class TicTacToeLayer : public Layer { public: - TicTacToeLayer(std::string); + // Derived from Layer class with input paramters for the game_uid, database + // and user_uid and overrides Layer::update(). + TicTacToeLayer(std::string, firebase::database::Database*, std::string); ~TicTacToeLayer(); - virtual void TicTacToeLayer::update(float); + + private: + // The game loop for this layer which runs every frame once scheduled using + // this->scheduleUpdate(). It constantly checks current_player_index_listener_ + // and game_over_listener so it can take action accordingly. + void TicTacToeLayer::update(float) override; + // Tracks whether the board was unable to build. - bool initialization_failed = false; + bool initialization_failed_ = false; + // Tracks the game outcome. - int game_outcome; - // Creating a string for the join game code and initializing the database + int game_outcome_; + + // String for the join game code and initialize the database // reference. - std::string join_game_uuid; - /// Firebase Auth, used for logging into Firebase. - firebase::auth::Auth* auth; + std::string join_game_uuid_; + + // User uid to update the user's record after the game is over. + std::string user_uid_; - /// Firebase Realtime Database, the entry point to all database operations. - firebase::database::Database* database; + // Firebase Realtime Database, the entry point to all database operations. + firebase::database::Database* database_; + firebase::database::DatabaseReference ref_; - firebase::database::DatabaseReference ref; - // Creating listeners for database values. // The database schema has a top level game_uuid object which includes - // last_move, total_players and current_player_index fields. - std::unique_ptr current_player_index_listener; - std::unique_ptr last_move_listener; - std::unique_ptr total_player_listener; - std::unique_ptr game_over_listener; - // Creating lables and a sprites. - Sprite* board_sprite; - Sprite* leave_button_sprite; - cocos2d::Label* game_over_label; - cocos2d::Label* waiting_label; - // Creating firebase futures for last_move and current_player_index - Future future_last_move; - Future future_current_player_index; - Future future_game_over; - // Creating the board, remaining available tile set and player index - // variables. - int current_player_index; - int player_index; - bool awaiting_opponenet_move; + // last_move, total_players and current_player_index_ fields. + + // Listeners for database values. + std::unique_ptr current_player_index_listener_; + std::unique_ptr last_move_listener_; + std::unique_ptr total_player_listener_; + std::unique_ptr game_over_listener_; + + // Lables and a sprites. + Sprite* board_sprite_; + Sprite* leave_button_sprite_; + cocos2d::Label* game_over_label_; + cocos2d::Label* waiting_label_; + + // Firebase futures for last_move and current_player_index_. + Future future_last_move_; + Future future_current_player_index_; + Future future_game_over_; + + int current_player_index_; + int player_index_; int board[kTilesX][kTilesY]; - std::unordered_set remaining_tiles; - int end_game_frames = 0; + + bool awaiting_opponenet_move_; + + // Unordered set of remaining tiles available for player moves. + std::unordered_set remaining_tiles_; + + // Amount of frames the screen has been in the end game state. + int end_game_frames_ = 0; }; #endif // TICTACTOE_DEMO_CLASSES_TICTACTOELAYER_SCENE_H_ diff --git a/demos/TicTacToe/Classes/TicTacToeScene.cpp b/demos/TicTacToe/Classes/TicTacToeScene.cpp index 127b25d3..054b8abf 100644 --- a/demos/TicTacToe/Classes/TicTacToeScene.cpp +++ b/demos/TicTacToe/Classes/TicTacToeScene.cpp @@ -1,19 +1,33 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #include "TicTacToeScene.h" -#include "MainMenuScene.h" -#include "TicTacToeLayer.h" -#include "cocos2d.h" using cocos2d::Scene; -Scene* TicTacToe::createScene(const std::string& game_uuid) { +Scene* TicTacToe::createScene(const std::string& game_uuid, + firebase::database::Database* main_menu_database, + const std::string& main_menu_user_uid) { // Sets the join_game_uuid to the passed in game_uuid. // Builds a simple scene that uses the bottom left cordinate point as (0,0) // and can have sprites, labels and layers added onto it. Scene* scene = Scene::create(); // Builds a layer to be placed onto the scene which has access to TouchEvents. - // This TicTacToe layer being created is owned by scene. - auto tic_tac_toe_layer = new TicTacToeLayer(game_uuid); + // This TicTacToe layer created is owned by the scene. + auto tic_tac_toe_layer = + new TicTacToeLayer(game_uuid, main_menu_database, main_menu_user_uid); scene->addChild(tic_tac_toe_layer); return scene; diff --git a/demos/TicTacToe/Classes/TicTacToeScene.h b/demos/TicTacToe/Classes/TicTacToeScene.h index ffdc8651..6593dd3f 100644 --- a/demos/TicTacToe/Classes/TicTacToeScene.h +++ b/demos/TicTacToe/Classes/TicTacToeScene.h @@ -1,13 +1,32 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #ifndef TICTACTOE_DEMO_CLASSES_TICTACTOE_SCENE_H_ #define TICTACTOE_DEMO_CLASSES_TICTACTOE_SCENE_H_ +#include "MainMenuScene.h" +#include "TicTacToeLayer.h" #include "cocos2d.h" class TicTacToe : public cocos2d::Layer { public: // Builds a simple scene that uses the bottom left cordinate point as (0,0) // and can have sprites, labels and nodes added onto it. - static cocos2d::Scene* createScene(const std::string&); + static cocos2d::Scene* createScene(const std::string&, + firebase::database::Database*, + const std::string&); + // Defines a create type for a specific type, in this case a Layer. CREATE_FUNC(TicTacToe); }; diff --git a/demos/TicTacToe/Resources/logout.png b/demos/TicTacToe/Resources/logout.png new file mode 100644 index 00000000..9f0531e8 Binary files /dev/null and b/demos/TicTacToe/Resources/logout.png differ