Skip to content

Instantly share code, notes, and snippets.

@yoavst
Created November 11, 2019 09:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yoavst/ace6847b3f8a46a5748874445c7e8230 to your computer and use it in GitHub Desktop.
Save yoavst/ace6847b3f8a46a5748874445c7e8230 to your computer and use it in GitHub Desktop.
Aluf Hamikraot bot
package com.yoavst.testing.project
import android.content.Intent
import android.widget.ImageView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.*
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AlufWinner {
private lateinit var device: UiDevice
private lateinit var game: GameController
@Before
fun launchApp() {
// Initialize UiDevice instance
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
game = GameController(device)
openApp()
}
private fun openApp() {
if (SHOULD_OPEN_APP) {
// Start from the home screen
device.pressHome()
// Wait for launcher
val launcherPackage = device.launcherPackageName
MatcherAssert.assertThat(launcherPackage, Matchers.notNullValue())
device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT)
// Launch the app
val context = InstrumentationRegistry.getInstrumentation().targetContext
val intent = context.packageManager.getLaunchIntentForPackage(PACKAGE_NAME)
?: throw IllegalStateException("Application is not installed")
// Clear out any previous instances
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
// Wait for the app to appear
device.wait(Until.hasObject(By.pkg(PACKAGE_NAME).depth(0)), LAUNCH_TIMEOUT * 2)
// Open game activity
val gamesModeBtn =
device.findObject(UiSelector().className(ImageView::class.java).instance(2))
gamesModeBtn.clickAndWaitForNewWindow()
}
}
private fun startFirstGame() {
if (SHOULD_OPEN_APP) {
val playBtn = device.findObject(UiSelector().text("שחק"))
playBtn.clickAndWaitForNewWindow()
}
}
@Test
fun runBot() {
startFirstGame()
while (true) {
trying(game::tryPlayRound)
game.tryPlayAgain()
game.tryCloseAd()
}
}
}
private const val PACKAGE_NAME = "org.apache.cordova.Mikraot"
private const val LAUNCH_TIMEOUT = 5000L
private const val SHOULD_OPEN_APP = true
private inline fun trying(func: () -> Unit) {
try {
func()
} catch (e: UiObjectNotFoundException) {
e.printStackTrace()
} catch (e: StaleObjectException) {
e.printStackTrace()
}
}
package com.yoavst.testing.project
import android.util.Log
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
private val strangeQuestions = setOf(
"איזה מהבאים חיבר הרמב\"ם?",
"איזו מצווה נהוגה בפורים?",
"מה מהבאים עמד בבסיס האידיאולוגיה הנאצית?",
"מדוע הוקמה \"ועדת פיל\"?",
"מהי ''התיוונות''?"
)
class Competitor(private val cache: MutableMap<Question, String> = mutableMapOf()) {
/**
* Tries to answer a question.
* If it has already seen the question, provides the right answer
*/
operator fun get(question: Question): Int? {
if (question.question in strangeQuestions) {
return when (question.question) {
"מה מהבאים עמד בבסיס האידיאולוגיה הנאצית?" -> if ("הצורך במרחב מחייה" in question.answers && "השמדת יהודים" !in question.answers && "טיהור עולמי" !in question.answers) {
question.answers.indexOf("הצורך במרחב מחייה")
} else {
question.answers.indexOf("כל התשובות נכונות")
}
"מדוע הוקמה \"ועדת פיל\"?" -> if ("כדי להציע פתרון לסכסוך היהודי-ערבי" in question.answers) {
question.answers.indexOf("כדי להציע פתרון לסכסוך היהודי-ערבי")
} else {
question.answers.indexOf("בעקבות מאורעות תרצ\"ו")
}
"איזו מצווה נהוגה בפורים?" -> if ("קריאת המגילה" in question.answers) {
question.answers.indexOf("קריאת המגילה")
} else {
question.answers.indexOf("מתנות לאביונים")
}
"איזה מהבאים חיבר הרמב\"ם?" -> if ("משנה תורה" in question.answers) {
question.answers.indexOf("משנה תורה")
} else {
question.answers.indexOf("מורה נבוכים")
}
"מהי ''התיוונות''?" -> question.answers.indexOf("קבלת התרבות היוונית וישומה")
else -> -1
}
}
if (question in cache) {
val index = question.answers.indexOf(cache[question])
if (index >= 0)
return index
Log.w("aluf-bot", "Conflict!!! $question ${cache[question]}")
return -1
}
return null
}
/**
* Updates a question with the given answer
*/
operator fun set(question: Question, answer: Int) {
if (question !in cache) {
Log.i("aluf-bot", "Database size is ${cache.size}")
}
cache[question] = question.answers[answer]
}
fun export(): String {
return Gson().toJson(cache.toList())
}
companion object {
fun of(dump: String) =
Competitor(Gson().fromJson<List<Pair<Question, String>>>(dump).associate { it }.toMutableMap())
}
}
data class Question(val question: String, val answers: List<String>) {
override fun hashCode(): Int = question.hashCode()
override fun equals(other: Any?) = other is Question && question == other.question
}
package com.yoavst.testing.project
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import androidx.core.graphics.get
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import java.io.File
import java.lang.Thread.sleep
class GameController(
private val device: UiDevice,
private val competitor: Competitor = Competitor.of(DATA_FILE.readText())
) {
private var answeredQuestions = 0
private val questionsSeen = mutableSetOf<String>()
fun tryPlayRound() {
if (!device.hasObject(By.textContains("שניות")))
return
val buttonOffset = if (device.hasObject(By.textContains("ספונסר"))) 1 else 0
// get question
val questionText =
device.findObjects(By.clickable(true).clazz(ImageView::class.java))
.firstOrNull { it.visibleBounds.left == LEFT_OF_ANSWER }?.parent?.text ?: return
// get answers
val answerButtons = (buttonOffset..(ANSWERS_COUNT - 1 + buttonOffset)).map {
UiSelector().clickable(true).className(ImageView::class.java).instance(it)
}.map(device::findObject)
val answers = answerButtons.map(UiObject::getText)
if (answers.any(String::isBlank)) {
Log.w(LOG_TAG, "Warning: blank answer")
return
}
// Answer the question
val question = Question(questionText, answers)
val selectedAnswer = competitor[question]
if (selectedAnswer == null || selectedAnswer >= 0) {
val (x, y) = ANSWERS_POINTS[selectedAnswer ?: RANDOM_GUESS].first()
device.click(x, y)
} else return
sleep(TIMEOUT_WAIT_FOR_RESULT_FOR_ANSWERING)
// check what is the correct answer if possible
assert(device.takeScreenshot(BITMAP_FILE))
val bitmap = BitmapFactory.decodeFile(BITMAP_FILE.absolutePath)
val correctAnswer =
ANSWERS_POINTS.indexOfFirst { it.any { (x, y) -> bitmap[x, y] == CORRECT_ANSWER_COLOR } }
.takeIf { it != -1 }
if (correctAnswer != null) {
competitor[question] = correctAnswer
}
// report to log
Log.i(LOG_TAG, "========= $question ===========")
Log.i(LOG_TAG, "${++answeredQuestions}) correct: $correctAnswer, answered: $selectedAnswer")
if (selectedAnswer != null && correctAnswer != null && selectedAnswer != correctAnswer) {
Log.w(LOG_TAG, "Mismatch: cached answer and correct answer are different")
}
if (correctAnswer == null) {
Log.w(LOG_TAG, "Fail: Could not capture correct answer")
}
if (question.question in questionsSeen) {
Log.w(LOG_TAG, "Repeat: question was seen already")
}
questionsSeen += question.question
Log.i(LOG_TAG, "====================================")
}
fun tryCloseAd() {
val xButton = device.findObject(
By.clazz(Button::class.java).clickable(true).hasChild(
By.clazz(Button::class.java)
)
)
if (xButton != null && xButton.childCount == 1 && xButton.parent.childCount == 2) {
xButton.children[0].click()
}
}
fun tryPlayAgain() {
device.findObject(By.text("שחק שוב"))?.let {
DATA_FILE.writeText(competitor.export())
it.click()
}
}
companion object {
private const val TIMEOUT_WAIT_FOR_RESULT_FOR_ANSWERING = 100L
private const val LOG_TAG = "Aluf-bot"
private const val ANSWERS_COUNT = 4
private const val RANDOM_GUESS = 2
private const val LEFT_OF_ANSWER = 48
private val ANSWERS_POINTS = listOf(
listOf(184 to 830),
listOf(184 to 1015),
listOf(184 to 1200),
listOf(184 to 1385)
)
private val CORRECT_ANSWER_COLOR = Color.parseColor("#2CB888")
private val BITMAP_FILE = File("/sdcard/bot_aluf_hamikraot.png")
private val DATA_FILE = File("/sdcard/bot_aluf_hamikraot.json")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment